这里是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();} }
网络编程的知识到这里就差不多结束了~~~
下节我们将进入跟多姿多彩的网络世界里学习~~~
毕竟不知后事如何,且听下回分解
❤️❤️❤️❤️❤️❤️❤️