Java 网络编程 ②-TCP Socket
这里是Themberfue
在上一节中,我们简单认识了 TCP协议 和 UDP协议 以及 基于UDP Socket 编写了简单的网络通信代码
本节我们将基于 TCP Socket 编写简单的网络通信代码
TCP Socket
类似 UDP Socket,Java也基于 TCP协议 进行了一些接口的封装
基于 TCP 封装的 SocketAPI:
ServerSocket:ServerSocket 是创建TCP服务端Socket的API。
方法签名 方法说明 ServerSocket(int port) 创建⼀个服务端流套接字Socket,并绑定到指定端⼝ ServerSocket提供的方法
方法签名 方法说明 Socket accept() 开始监听指定端口(创建时绑定的端⼝),有客户端连接后,返回⼀个服务端Socket对象,并基于该 Socket建立与客户端的连接,否则阻塞等待 void close() 关闭此套接字 Socket:Socket是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
方法签名 方法说明 Socket(String host, int port) 创建⼀个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 Socket提供的方法
方法签名 方法说明 InetAddressgetInetAddress() 返回套接字所连接的地址 InputStreamgetInputStream() 返回此套接字的输入流 OutputStreamgetOutputStream() 返回此套接字的输出流
代码编写与讲解
TcpEchoServer
import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @author: Themberfue * @date: 2024/11/14 19:24 * @description: */ public class TcpEchoServer { // TCP 协议专门用于创建服务端的对象 private ServerSocket socket = null; // 这里和 UDP 服务器类似,也是在构造对象时,绑定端口号 public TcpEchoServer(int port) throws IOException { this.socket = new ServerSocket(port); } public void start() throws IOException { System.out.println("启动服务器"); // 这种情况一般不会用 fixedThreadPool,不然处理客户端请求的数量就固定了 ExecutorService executorService = Executors.newCachedThreadPool(); // 需要一直执行里面的逻辑 // 一直保持去连接客户端的状态 while (true) { // 对于 TCP 来说,需要先处理和客户端发来的连接(有连接) // 通过读写 clientSocket,与客户端进行通信 // 如果没有客户端发起连接,那么就阻塞在 accept // 这里的阻塞和多线程的阻塞有本质区别 // 如果只是一个线程处理连接和读取请求两个工作 // 该线程会在 hasNext() 那里阻塞,就不能处理下一个客户端的连接了 // 所以引入多线程来处理,这里的主线程处理与客户端的连接 // 连接到一个客户端后,就创建一个线程来处理客户端发送来的请求 Socket clientSocket = socket.accept(); // Thread t = new Thread(() -> { // processConnection(clientSocket); // }); // t.start(); // 反复创建的线程,开销还是很大的,所以使用线程池来处理请求更高效 executorService.submit(() -> { processConnection(clientSocket); }); } } // 处理一个客户端的连接 // 可能涉及到多个客户端的请求和响应 private void processConnection(Socket clientSocket) { // 建立连接成功后,打印客户端上线,表示与客户端建立连接 System.out.printf("[%s:%d]:客户端上线\n", clientSocket.getInetAddress(), clientSocket.getPort()); // TCP 是通过面向字节流的 try (OutputStream outputStream = clientSocket.getOutputStream(); InputStream inputStream = clientSocket.getInputStream()) { // 针对 OutputStream 套了一层 PrintWriter printWriter = new PrintWriter(outputStream); // 针对 InputStream 套了一层 Scanner scanner = new Scanner(inputStream); // 这里也是反复执行,一个客户端可能发来多次请求 while (true) { // 若该客户端关闭后,表示不会发送请求,自然就读不到请求了,此时,该客户端下线 // 若一直没有读到请求,就在这里阻塞,直到客户端那边发来请求 if (!scanner.hasNext()) { // 断开连接了 System.out.printf("[%s:%d]:客户端下线\n", clientSocket.getInetAddress(), clientSocket.getPort()); break; } // 1. 读取请求并解析 String request = scanner.next(); // 2. 根据请求计算响应 String response = process(request); // 将计算后的响应发送给客户端 printWriter.println(response); // flush 起到刷新缓冲区的作用 printWriter.flush(); // 打印日志 System.out.printf("[%s:%d] req:%s res:%s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response); } } catch (IOException e) { throw new RuntimeException(e); } finally { try { // 该客户端断开后,就得关闭 clientSocket.close(); } catch (IOException e) { throw new RuntimeException(e); } } } private String process(String request) { return request; } public static void main(String[] args) throws IOException { TcpEchoServer tcpEchoServer = new TcpEchoServer(9090); tcpEchoServer.start(); } }
与 UDP 不同的是,TCP协议是 有连接,所以在进行网络通信前,需要客户端和服务器端进行连接,交换彼此的一些信息。
服务器端的 ServerSocket 通过 accept 方法尝试与客户端 Socket 连接;如果连接成功,则返回一个 Socket对象;没连接到的话,就一直阻塞在这里,直到连接到客户端。
与 UDP 又不同的是,TCP 是以字节为单位进行读写数据的,故使用 InputStream 和 OutputStream 进行读写数据。
由于客户端可能不止发送一个请求,所以也是使用死循环,反复尝试读取数据。
若一直没有读到请求,则会在 hasNext() 这里阻塞,直到客户端发送请求,成功读取到数据。
写入数据时,必须使用 println,因为这里隐含的表示以 \n 为结束符号结束写入,若没有这一符号,则程序不知道什么时候停止写入。
在写入时,并不是直接写入到网卡的,而是写入到缓冲区,故在写完时,还需刷新缓冲区,确保数据成功写入到网卡上。
在客户端断开连接后,需要关闭为客户端创建的 Socket 连接
对于 TCP协议 来说,需要先处理和客户端发来的连接,通过读写 clientSocket,与客户端进行通信,如果没有客户端发起连接,那么就会阻塞在 accept,这里的阻塞和多线程的阻塞又本质区别
如果只是一个线程处理连接和读取请求两个工作,由于服务端线程会在 hasNext() 那里阻塞,就不能处理下一个客户端的连接了,所以引入多线程来处理,这里的主线程处理与客户端的连接,连接到一个客户端后,就创建一个线程来处理客户端发送来的请求。
但是频繁的创建和销毁线程,开销很大,所以引入线程池来处理客户端的请求。
TcpEchoClient
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; /** * @author: Themberfue * @date: 2024/11/14 19:37 * @description: */ public class TcpEchoClient { private Socket socket = null; public TcpEchoClient(String serverIp, int port) throws IOException { // TCP 就不需要单独创建额外的变量来保存服务器的ip和端口了 // 通过连接会自动保存对端的信息 // 直接把字符串的IP的值,设置过来 this.socket = new Socket(serverIp, port); } public void start() { Scanner scanner = new Scanner(System.in); try (OutputStream outputStream = socket.getOutputStream(); InputStream inputStream = socket.getInputStream()) { // 同样为了使用方便,套壳使用 Scanner scannerNet = new Scanner(inputStream); PrintWriter printWriter = new PrintWriter(outputStream); // 客户端可能发送的请求不止一次,所以通过死循环的逻辑(简单的从控制台读取,所以这样) // 该客户端进程结束时,请求就不发了,自然服务端那边就不能读取到了 // 这边这个客户端连接就与服务端断开了 while (true) { // 从控制台读取客户端的请求 String request = scanner.next(); // 将客户端的请求发送到服务端 // 这个操作只是将数据写入到 "缓冲区",并没有真正写入网卡中 printWriter.println(request); // 这里清除缓冲区后,才真正将数据写入到网卡中 printWriter.flush(); // 读取服务端发送来的请求 String response = scannerNet.next(); System.out.printf("req:%s res:%s\n", request, response); } } catch (IOException e) { throw new RuntimeException(e); } } public static void main(String[] args) throws IOException { TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1", 9090); tcpEchoClient.start(); } }
网络编程的知识到这里就差不多结束了~~~
下节我们将进入跟多姿多彩的网络世界里学习~~~
毕竟不知后事如何,且听下回分解
❤️❤️❤️❤️❤️❤️❤️