一、一些相关的概念解释
我们知道在网络编程中经常会涉及到各种各样的概念,我们来重温一下这些概念
在进行网络编程中,我们经常会用到操作系统给我们提供的网络编程API(这些api也叫socket api),这些原生的API大多是c语言,于是在我们java中,JVM非常贴心的把这些c风格的 socket api封装了一下,变成了我们Java中面向对象风格的api
- 在我们网络的TCP/IP五层(或四层)模型中正是在其中的传输层,提供了网络通信api —— socket api
- 传输层提供了两个非常重要的协议,按照不同的协议TCP和UDP,这两个协议所对应的socket api也是截然不同的
🍑UDP协议所对应的api
二、实例分析
1、UDP
🌰 UDP版本的 回显服务器-客户端
接下来我们通过一个实例来演示这些api的具体用法
比如我们现在要写一个UDP版本的 回显服务器 + 客户端的这样一个交互程序(客户端发什么,服务器就返回什么)
对于UDP协议来说,具有无连接,面向数据报的特征,即每次都是没有建立连接,并且一次发送全部数据报,一次接收全部的数据报。
java中使用UDP协议通信,主要基于 DatagramSocket 类来创建数据报套接字,并使用
DatagramPacket 作为发送或接收的UDP数据报
结合上图我们也可以分析出来该实现服务器和客户端之间回显操作的基本流程
服务器代码:
package network; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketException; // 回显服务器 public class UdpEchoServer { // 要创建一个服务器,先打开一个socket文件 private DatagramSocket socket = null; public UdpEchoServer(int port) throws SocketException { socket = new DatagramSocket(port); } // 开始创建服务器,这里我们是回显服务器 public void start() throws IOException { System.out.println("开始启动服务器!"); while (true) { // 1、尝试接收(读取)客户端的请求,传输数据是以 DatagramPacket为基本单位 DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096); socket.receive(requestPacket); // 把接收到的数据包放到该方法的参数中(输出型参数)如果此时客户端没有发送请求,就该方法就陷入阻塞等待中 // 2、对请求进行解析,把DatagramPacket转成一个String String request = new String(requestPacket.getData(), 0, requestPacket.getLength()); // 3.处理请求 String response = process(request); // 4、把响应构造成DatagramPacket对象 // 构造响应对象,要搞清楚,对象要发给谁,谁给咱们发的请求,咱就发给谁,response.length返回的是字符个数,response.getBytes().length返回的是字节个数 DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, requestPacket.getSocketAddress()); // 5、发送响应给客户 socket.send(responsePacket); // 打印日志, requestPacket.getAddress()返回读取的客户端IP地址, requestPacket.getPort()返回读取的客户端端口号 System.out.printf("发送方的ip和端口号:[%s:%d] request:%s, response:%s", requestPacket.getAddress().toString(), requestPacket.getPort(), request, response); System.out.println(); } } // 对请求进行响应处理 public String process(String request) { return request; // 回显处理就是请求发的是什么就响应什么 } public static void main(String[] args) throws IOException { UdpEchoServer udpEchoServer = new UdpEchoServer(8000); udpEchoServer.start(); } }
客户端代码
package network; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; import java.nio.charset.StandardCharsets; import java.util.Scanner; public class UdpEchoClient { private DatagramSocket socket = null; public UdpEchoClient() throws SocketException { socket = new DatagramSocket(); // 在客户端,我们一般不自己指定端口号,而是由系统分配 } public void start() throws IOException { while (true) { System.out.println("当前是客户端!"); System.out.print(">"); // 1、客户端从键盘输入请求的具体内容 Scanner scanner = new Scanner(System.in); String request = scanner.nextLine(); // 要说明要给谁发送请求,他的ip地址和端口号是多少(服务器的) // DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.length(), InetAddress.getByName("127.0.0.1"), 8000); DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName("127.0.0.1"), 8000); // 2、客户端向服务器发送请求 socket.send(requestPacket); // 3、客户端接收服务器返回的响应 DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096); socket.receive(responsePacket); // 注意我们是把服务器的响应存放到了responsePacket中,不要写错 // 4、客户端显示服务器返回的响应信息,进行解析转换成String类型的信息 String response = new String(responsePacket.getData(), 0, responsePacket.getLength()); System.out.printf("发送方的ip地址和端口号:[%s:%s], request: %s, response: %s\n", requestPacket.getAddress(), requestPacket.getPort(), request, response); } } public static void main(String[] args) throws IOException { UdpEchoClient client = new UdpEchoClient(); client.start(); } }
一个服务器可以为多个客户端提供服务
🌰UDP版本的 字典服务器-客户端
不同服务器他们的读取请求并解析、构造响应并返回的整体大逻辑是一样的,所不同就就在于对请求的处理,一个服务器要完成的工作,都是通过”根据请求计算响应“来体现的,这个步骤也是服务器的”灵魂所在“
- 我们的字典服务器完全可以在上述回显服务器的基础上完成工作——只需要改变那个根据请求来计算响应的步骤。
- 至于客户端,其实和上述回显服务器的客户端是一样的——都是发不同的请求而已,连改都不用改
服务器代码:
package network; import java.io.IOException; import java.net.DatagramSocket; import java.net.SocketException; import java.util.HashMap; import java.util.Map; // 字典服务器 public class UdpDicServer extends UdpEchoServer { public Map<String, String> map = new HashMap<>(); public UdpDicServer(int port) throws SocketException { // 构造方法 super(port); map.put("synchronized", "同步的"); map.put("volatile", "易变的"); } @Override public String process(String request) { return map.getOrDefault(request, "抱歉,当前尚未收录此词!"); } public static void main(String[] args) throws IOException { UdpDicServer server = new UdpDicServer(8000); server.start(); } }
客户端代码:
package network; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; import java.nio.charset.StandardCharsets; import java.util.Scanner; public class UdpEchoClient { private DatagramSocket socket = null; public UdpEchoClient() throws SocketException { socket = new DatagramSocket(); // 在客户端,我们一般不自己指定端口号,而是由系统分配 } public void start() throws IOException { while (true) { System.out.println("当前是客户端!"); System.out.print(">"); // 1、客户端从键盘输入请求的具体内容 Scanner scanner = new Scanner(System.in); String request = scanner.nextLine(); // 要说明要给谁发送请求,他的ip地址和端口号是多少(服务器的) // DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.length(), InetAddress.getByName("127.0.0.1"), 8000); DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName("127.0.0.1"), 8000); // 2、客户端向服务器发送请求 socket.send(requestPacket); // 3、客户端接收服务器返回的响应 DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096); socket.receive(responsePacket); // 注意我们是把服务器的响应存放到了responsePacket中,不要写错 // 4、客户端显示服务器返回的响应信息,进行解析转换成String类型的信息 String response = new String(responsePacket.getData(), 0, responsePacket.getLength()); System.out.printf("发送方的ip地址和端口号:[%s:%s], request: %s, response: %s\n", requestPacket.getAddress(), requestPacket.getPort(), request, response); } } public static void main(String[] args) throws IOException { UdpEchoClient client = new UdpEchoClient(); client.start(); } }
客户端测试:
服务器测试:
2、TCP
🌰 TCP版本的 回显服务器-客户端 (多线程+线程池版本)
服务器代码
package network; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TcpEchoServer { ServerSocket serverSocket = null; public TcpEchoServer(int port) throws IOException { serverSocket = new ServerSocket(port); } public void start() throws IOException { ExecutorService service = Executors.newCachedThreadPool();// 此处不太适合使用 "固定个数的" while (true) { // 服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket // 如果当前没有客户端来建立连接, 就会阻塞等待!! Socket clientSocket = serverSocket.accept(); // processConnection(clientSocket); // 单线程模式——即该服务器无法同时为多个客户端同时提供服务 // [版本2] 多线程版本. 主线程负责拉客, 新线程负责通信 // 虽然比版本1 有提升了, 但是涉及到频繁创建销毁线程, 在高并发的情况下, 负担比较重的. // Thread thread = new Thread(() -> { // try { // processConnection(clientSocket); // } catch (IOException e) { // e.printStackTrace(); // } // }); // thread.start(); // [版本3] 使用线程池, 来解决频繁创建销毁线程的问题. // 此处不太适合使用 "固定个数的" service.submit(new Runnable() { @Override public void run() { try { processConnection(clientSocket); } catch (IOException e) { e.printStackTrace(); } } }); } } // 通过这个方法, 给当前连上的这个客户端, 提供服务!! // 这里我们建立长连接——一次连接中,会有 N次请求,N次响应 public void processConnection(Socket clientSocket) throws IOException { System.out.printf("连接成功![%s:%d]\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream() ) { Scanner scannerNet = new Scanner(inputStream); // 对我们的读取进行嵌套 PrintWriter printWriter = new PrintWriter(outputStream); while (true) { if (!scannerNet.hasNext()) { // 说明此时连接中断,服务器无法继续从客户端读取到请求 // 连接断开. 当客户端断开连接的时候, 此时 hasNext 就会返回 false System.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); break; } // 1、读取客户端的请求 String request = scannerNet.nextLine(); // 从客户端读取到的请求,如果客户端在输入请求的时候,只是通过nextLine敲了回车,然后就通过printWrite.write发送给我们的服务器, // 这时服务器接收到的数据是不带换行符的,换行符在我们的客户端nextLine输入的过程中就已经被吞吃了,所有为了避免String request = scanner.nextLine();无法正常读取(nextLine只有遇到换行才结束) // 所以在我们的客户端在发送时要通过PrintWrite.println()来发送,会给我们自动添加一个换行符 // 2、处理客户端发来的请求 String response = process(request); // 3、把响应发送给客户端 printWriter.println(response); // printWriter.write(response); // 这样写有bug,因为我们当前的response中没有带换行符, printWriter.flush(); // 刷新缓冲区 // 打印日志 System.out.printf("[%s:%d] request: %s, response: %s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response); } } finally { clientSocket.close(); // 每一次建立连接都会创建一个clientSocket,该连接结束后要及时关闭,避免内存泄漏 } } public String process(String request) { return request; } public static void main(String[] args) throws IOException { TcpEchoServer server = new TcpEchoServer(8000); server.start(); } }
客户端代码
package network; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; public class TcpEchoClient { Socket socket = null; public TcpEchoClient() throws IOException { // 指定客户端要连接的服务器 socket = new Socket("127.0.0.1", 8000); // 程序走到了这里,说明客户端和服务器就已经成功建立了连接 } public void start() throws IOException { Scanner scanner = new Scanner(System.in); System.out.println("当前是客户端,请输入请求"); try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) { // InputStream用于读取, OutputStream用于发送 // 对我们的读取和发送进行写包装,使得读取和包装更方便 Scanner scannerNet = new Scanner(inputStream); PrintWriter printWriter = new PrintWriter(outputStream); // 这里我们建立长连接——一次连接中,会有 N次请求,N次响应 while (true) { System.out.print("> "); // 1、客户端从键盘输入请求 String request = scanner.nextLine(); // 在这里我们虽然在输入内容后用换行符来进行了结束,但我们用于接收的request并没有读取的换行符——它被nextLine()给吞吃了 // 2、把请求发送给服务器 printWriter.println(request); // printWriter.write(request); // 这么写会产生一个bug, 要printWriter.println()会自动给我们要写入的添加一个换行符 // 可以让服务器在读取请求时遇到换行就结束,不至于陷入阻塞 printWriter.flush(); // 刷新缓冲区 // 3、从服务器读取响应内容 String response = scannerNet.nextLine(); // 4、打印日志 System.out.printf("request: %s, response: %s\n", request, response); } } finally { socket.close(); } } public static void main(String[] args) throws IOException { TcpEchoClient client = new TcpEchoClient(); client.start(); } }
测试