网络编程前言前置认知Socket 与 TCP、UDP核心区别什么是SocketTCP 与 UDP 四大核心差异UDP SocketDatagramSocketDatagramPacket基础UDP 回显服务器Echo SeverUDP 服务端父类 UdpEchoServerUDP 客户端 UdpEchoClient扩展继承实现 UDP 词典翻译服务器TCP SocketServerSocketSocket单线程 TCP 回显服务器TCP 服务端 TcpEchoServerTCP 客户端 TcpEchoClient单线程 TCP 服务器致命缺陷无法并发处理多客户端优化方案一多线程 TCP 服务器优化方案二线程池 TCP 服务器总结前言上一篇我们完整梳理了计算机网络底层理论局域网与广域网组网、IP 与端口、TCP/IP 五层模型、数据封装与解封装全过程。本篇正式落地 Java 网络编程核心 ——Socket API手把手实现两套完整通信程序UDP 无连接通信基础回显服务器、继承扩展英文词典翻译服务器TCP 面向连接通信单线程阻塞缺陷 → 多线程并发改造 → 线程池最终优化。同时详解 TCP 与 UDP 四大核心区别、Socket 本质、文件描述符泄漏、并发阻塞经典问题是 Java 网络编程入门必练代码。前置认知Socket 与 TCP、UDP核心区别什么是SocketSocket 是操作系统提供给应用层的网络编程API本质是操作系统中一个特殊文件将网卡硬件抽象成文件向 Socket 文件写入数据 通过网卡发送网络数据包从 Socket 文件读取数据 通过网卡接收网卡数据包Java 中分为两套API分别对应传输层两大协议UDPDatagramSocket收发载体、DatagramPacketUDP数据包TCPServerSocket服务器监听端口、Socket客户端连接、数据读写。网络通信数据的基本单位Datagram 数据报、Packet 数据包、Frame 数据帧、Segment 数据段。TCP 与 UDP 四大核心差异对比维度TCPUDP连接特性有连接通信前必须建立连接(三次握手)无连接直接发送数据包不保存对方信息传输可靠性可靠传输不可靠传输传输单位面向字节流以字节为单位流式传输面向数据报以完整 DatagramPacket 数据包为单位传输通信模式全双工同一个连接可同时收发全双工同一个 Socket 可同时收发注意 可靠传输要付出代价TCP协议实现复杂传输效率更低UDP开销极小传输速度更快。对于应用程序客户端发起“建立连接”的动作。服务器把建好的连接从内核中拿到应用程序中。UDP SocketUDP 特点无连接、每次发送数据包必须在 DatagramPacket 中携带目的IP端口服务器固定端口客户端由系统随机分配端口。DatagramSocket使用 DatagramSocket 这个类来表示系统内部的Socket文件。void receive(DatagramPacket p)接收void send(DatagramPacket p)发送void close()DatagramPacket使用 DatagramPacket 这个类来表示一个UDP数据报。UDP是面向数据报的每次进行传输都要以UDP数据报DatagramPacket为基本单位。基础UDP 回显服务器Echo Sever功能客户端控制台输入字符串服务器原样返回。UDP 服务端父类 UdpEchoServerpackagenetwork;importjava.io.IOException;importjava.net.DatagramPacket;importjava.net.DatagramSocket;importjava.net.SocketException;importjava.nio.charset.StandardCharsets;//服务器publicclassUdpEchoServer{//创建一个 DatagramSocket 对象后续操作网卡的基础privateDatagramSocketsocketnull;publicUdpEchoServer(intport)throwsSocketException{socketnewDatagramSocket(port);}/* 服务器和客户端都需要创建 Socket 对象 1. 服务器的Socket一般要显示指定一个端口号此时Socket对象就能绑定到这个指定的端口 2. 客户端的Socket一般不能显示指定此时系统会自动分配一个随机的端口。 3. 服务器手动指定端口不会冲突因为一个服务器上都有哪些程序使用哪些端口都是可控的。但客户端不可控。 * */publicvoidstart(){//通过这个方法来启动服务器System.out.println(UDP回显服务器启动);//服务器7*24运行死循环持续处理请求while(true){//1.准备空缓冲区接收客户端数据包---读取请求并解析byte[]buffernewbyte[4096];DatagramPacketrequestPacketnewDatagramPacket(buffer,buffer.length);//阻塞等待客户端请求没有数据则线程挂起try{socket.receive(requestPacket);//由receive内部对数据进行填充数据以二进制形式存储到DatagramPacket中//2.解析数据包为字符串必须用实际收到长度getLength()StringrequestnewString(requestPacket.getData(),0,requestPacket.getLength());//3.业务处理回显逻辑交给process方法支持子类重写(一个服务器的核心步骤)Stringresponseprocess(request);//4.把响应写回客户端构造响应数据包携带客户端IP端口DatagramPacketresponsePacketnewDatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());//UDP 无连接自身不会保存数据要发送给谁就需要每次发送时重新指定数据要发到哪里去socket.send(responsePacket);//打印通信日志System.out.printf([%s:%d] req%s, resp%s%n,requestPacket.getAddress(),requestPacket.getPort(),request,response);}catch(IOExceptione){thrownewRuntimeException(e);}}}publicStringprocess(Strings){returns;}publicstaticvoidmain(String[]args)throwsSocketException{UdpEchoServerservernewUdpEchoServer(9090);server.start();}}思考Socket 也是文件为什么不写close也没出现文件泄露因为 Socket 是文件描述符表的一个表项每次打开一个文件就会占用一个位置文件描述符在PCB上跟随进程这个 Socket 在整个程序运行过程中都是需要使用的当它不再使用时就意味着程序要结束了进程结束随之文件描述符表就会销毁被系统自动回收。什么时候才会出现泄漏代码中频繁地打开文件但不关闭在一个进程的运行过程中不断积累打开的文件逐渐消耗掉文件描述符表中的内容最终消耗殆尽。若进程的生命周期很短则谈不上泄露。UDP 客户端 UdpEchoClientpackagenetwork;importjava.net.*;importjava.util.Scanner;publicclassUdpEchoClient{privateDatagramSocketsocketnull;privateStringserverIp ;privateintserverPort0;publicUdpEchoClient(Stringip,intport)throwsSocketException{//客户端不能手动指定端口系统自动分配随机空闲端口socketnewDatagramSocket();serverIpip;serverPortport;}publicvoidstart(){System.out.println(UDP客户端启动);ScannerscannernewScanner(System.in);while(true){//1.从控制台读取数据作为请求System.out.print(- );Stringrequestscanner.next();try{//2.构造请求数据包指定服务器IP端口DatagramPacketrequestPacketnewDatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(serverIp),serverPort);socket.send(requestPacket);//3.接收服务器响应byte[]buffernewbyte[4096];DatagramPacketresponsePacketnewDatagramPacket(buffer,buffer.length);socket.receive(responsePacket);//4.解析并打印响应StringresponsenewString(responsePacket.getData(),0,responsePacket.getLength());System.out.println(response);}catch(Exceptione){e.printStackTrace();}}}publicstaticvoidmain(String[]args)throwsSocketException{UdpEchoClientclientnewUdpEchoClient(127.0.0.1,9090);client.start();}}扩展继承实现 UDP 词典翻译服务器基于父类UdpEchoServer仅重写 process 方法通过 HashMap 实现英译汉字典查询。packagenetwork;importjava.net.SocketException;importjava.util.HashMap;importjava.util.Map;publicclassUdpDictServerextendsUdpEchoServer{privateMapString,StringdictnewHashMap();publicUdpDictServer(intport)throwsSocketException{super(port);//初始化词典dict.put(dog,小狗);dict.put(cat,小猫);dict.put(pig,小猪);}//重写process方法OverridepublicStringprocess(Strings){returndict.getOrDefault(s,词典中不存在该词语);}publicstaticvoidmain(String[]args)throwsSocketException{UdpDictServerservernewUdpDictServer(9090);server.start();}}TCP SocketTCP 是面向连接、面向字节流协议传输的基本单位是字节。ServerSocket给服务器使用的类使用这个类来绑定端口号。Socket既会给服务器使用也会给客户端使用。单线程 TCP 回显服务器TCP 服务端 TcpEchoServerimportjava.io.IOException;importjava.io.InputStream;importjava.io.OutputStream;importjava.io.PrintWriter;importjava.net.ServerSocket;importjava.net.Socket;importjava.util.Scanner;publicclassTcpEchoServer{privateServerSocketserverSocketnull;publicTcpEchoServer(intport)throwsIOException{serverSocketnewServerSocket(port);}publicvoidstart()throwsIOException{System.out.println(服务器启动);while(true){//阻塞等待客户端连接连接建立由内核三次握手自动完成完成后会在内核队列中排队SocketclientsocketserverSocket.accept();//处理当前客户端连接processConnection(clientsocket);}}//处理单个客户端完整通信逻辑privatevoidprocessConnection(SocketclientSocket){//打印日志表示当前有客户端连上了System.out.printf([%s:%d] 客户端上线%n,clientSocket.getInetAddress(),clientSocket.getPort());try(InputStreaminputStreamclientSocket.getInputStream();OutputStreamoutputStreamclientSocket.getOutputStream()){/* InputStream 和 OutputStream 就是字节流 通过 InputStream 进行 read 操作---接收 通过 OutputStream 进行 write 操作---发送 * */ScannerscannernewScanner(inputStream);PrintWriterprintWriternewPrintWriter(outputStream);while(true){if(!scanner.hasNext()){//没有数据/连接断开此时循环结束System.out.printf([%s:%d] 客户端下线%n,clientSocket.getInetAddress(),clientSocket.getPort());break;}//1.读取请求(以空白符、换行作为分隔)Stringrequestscanner.next();//2.根据请求计算响应Stringresponseprocess(request);//3.把响应写回到客户端printWriter.println(response);//刷新缓冲区强制把内存数据写入网卡printWriter.flush();System.out.printf([%s:%d] req%s, resp%s%n,clientSocket.getInetAddress(),clientSocket.getPort(),request,response);}}catch(IOExceptione){thrownewRuntimeException(e);}finally{// 确保连接断开时关闭Socket释放文件描述符// 这里需要close是因为当前方法processConnection就是在处理一个连接当该方法结束时连接要断开clientSocket在循环中每次有一个新的客户端进来建立连接都会创建出新的clientSocket//DatagramSocketServerSocket在程序中只有一个对象不close也没事try{clientSocket.close();}catch(IOExceptione){e.printStackTrace();}}}publicStringprocess(Stringrequest){returnrequest;}publicstaticvoidmain(String[]args)throwsIOException{TcpEchoServerservernewTcpEchoServer(9090);server.start();}}TCP 客户端 TcpEchoClientimportjava.io.IOException;importjava.io.InputStream;importjava.io.OutputStream;importjava.io.PrintWriter;importjava.net.Socket;importjava.util.Scanner;publicclassTcpEchoClient{privateSocketsocketnull;//在创建Socket的同时和服务器建立连接若连接建立失败则对象new失败publicTcpEchoClient(StringserverIp,intserverPort)throwsIOException{// new Socket时内核自动发起三次握手建立TCP连接socketnewSocket(serverIp,serverPort);}publicvoidstart(){ScannerscannernewScanner(System.in);try(InputStreaminputStreamsocket.getInputStream();OutputStreamoutputStreamsocket.getOutputStream()){PrintWriterprintWriternewPrintWriter(outputStream);ScannerscannerNextworknewScanner(inputStream);while(true){System.out.print(- );Stringrequestscanner.next();//发送请求printWriter.println(request);printWriter.flush();//读取响应StringresponsescannerNextwork.next();System.out.println(response);}}catch(IOExceptione){thrownewRuntimeException(e);}}publicstaticvoidmain(String[]args)throwsIOException{TcpEchoClientclientnewTcpEchoClient(127.0.0.1,9090);client.start();}}单线程 TCP 服务器致命缺陷无法并发处理多客户端单线程服务器start()方法只有一个主线程主线程调用accept()拿到第一个客户端连接后进入processConnection内部死循环如果第一个客户端一直不关闭连接、持续占用循环主线程会阻塞在scanner.hasNext()第二个客户端可以和服务器建立 TCP 连接内核完成三次握手与代码无关连接会存入内核等待队列但应用层永远无法调用accept()取出连接第二个客户端完全无法通信。优化方案一多线程 TCP 服务器每接收到一个客户端连接创建一个独立新线程处理通信逻辑主线程立刻回到accept()继续监听新连接实现并发。修改start():publicvoidstart()throwsIOException{System.out.println(TCP多线程服务器启动);while(true){SocketclientSocketserverSocket.accept();// 新建线程处理连接主线程非阻塞立刻循环下一次acceptnewThread(()-processConnection(clientSocket)).start();}}缺点高并发场景下频繁创建、销毁线程会带来巨大开销极端情况引发服务器卡顿。因此可以使用线程池优化。优化方案二线程池 TCP 服务器使用CachedThreadPool缓存线程复用空闲线程避免频繁创建销毁线程是生产环境基础方案。importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;publicvoidstart()throwsIOException{System.out.println(TCP线程池服务器启动);// 可缓存线程池自动扩容、复用空闲线程ExecutorServicepoolExecutors.newCachedThreadPool();while(true){SocketclientSocketserverSocket.accept();// 提交任务到线程池pool.submit(()-processConnection(clientSocket));}}总结UDP 数据包解析必须使用getLength()不能用getBytes().length字节数组缓冲区有多余空位getLength()代表本次实际接收的字节长度字节与字符串长度区分str.length()是字符个数str.getBytes()是字节个数包含中文时二者不一致网络传输必须统一使用字节TCP PrintWriter 必须 flush ()IO 缓冲区会积攒数据不刷新缓冲区数据不会真正发送到网卡TCP ClientSocket 必须 finally 关闭循环创建客户端连接不手动关闭会持续占用文件描述符引发泄漏单线程 TCP 天然不支持并发外层 accept 循环和内层读写循环在同一个线程必须通过多线程 / 线程池解耦UDP 无连接数据包必须携带完整目的地址TCP 面向连接连接建立后全程只需要读写流无需重复携带地址。本篇是「Java后端编程系列」的连载内容点击链接查看完整系列 上一篇网络初识基础入门局域网广域网、IP 端口、TCP/IP 五层模型与封装解封装全过程 下一篇网络三彻底搞懂网络底层五层模型、序列化、MAC/ARP、MTU 一次吃透 点击直达 Java后端基础专栏合集
网络(二)|Java Socket 编程全实战:UDP 回显 / 词典服务器、TCP 单线程→多线程线程池演进
网络编程前言前置认知Socket 与 TCP、UDP核心区别什么是SocketTCP 与 UDP 四大核心差异UDP SocketDatagramSocketDatagramPacket基础UDP 回显服务器Echo SeverUDP 服务端父类 UdpEchoServerUDP 客户端 UdpEchoClient扩展继承实现 UDP 词典翻译服务器TCP SocketServerSocketSocket单线程 TCP 回显服务器TCP 服务端 TcpEchoServerTCP 客户端 TcpEchoClient单线程 TCP 服务器致命缺陷无法并发处理多客户端优化方案一多线程 TCP 服务器优化方案二线程池 TCP 服务器总结前言上一篇我们完整梳理了计算机网络底层理论局域网与广域网组网、IP 与端口、TCP/IP 五层模型、数据封装与解封装全过程。本篇正式落地 Java 网络编程核心 ——Socket API手把手实现两套完整通信程序UDP 无连接通信基础回显服务器、继承扩展英文词典翻译服务器TCP 面向连接通信单线程阻塞缺陷 → 多线程并发改造 → 线程池最终优化。同时详解 TCP 与 UDP 四大核心区别、Socket 本质、文件描述符泄漏、并发阻塞经典问题是 Java 网络编程入门必练代码。前置认知Socket 与 TCP、UDP核心区别什么是SocketSocket 是操作系统提供给应用层的网络编程API本质是操作系统中一个特殊文件将网卡硬件抽象成文件向 Socket 文件写入数据 通过网卡发送网络数据包从 Socket 文件读取数据 通过网卡接收网卡数据包Java 中分为两套API分别对应传输层两大协议UDPDatagramSocket收发载体、DatagramPacketUDP数据包TCPServerSocket服务器监听端口、Socket客户端连接、数据读写。网络通信数据的基本单位Datagram 数据报、Packet 数据包、Frame 数据帧、Segment 数据段。TCP 与 UDP 四大核心差异对比维度TCPUDP连接特性有连接通信前必须建立连接(三次握手)无连接直接发送数据包不保存对方信息传输可靠性可靠传输不可靠传输传输单位面向字节流以字节为单位流式传输面向数据报以完整 DatagramPacket 数据包为单位传输通信模式全双工同一个连接可同时收发全双工同一个 Socket 可同时收发注意 可靠传输要付出代价TCP协议实现复杂传输效率更低UDP开销极小传输速度更快。对于应用程序客户端发起“建立连接”的动作。服务器把建好的连接从内核中拿到应用程序中。UDP SocketUDP 特点无连接、每次发送数据包必须在 DatagramPacket 中携带目的IP端口服务器固定端口客户端由系统随机分配端口。DatagramSocket使用 DatagramSocket 这个类来表示系统内部的Socket文件。void receive(DatagramPacket p)接收void send(DatagramPacket p)发送void close()DatagramPacket使用 DatagramPacket 这个类来表示一个UDP数据报。UDP是面向数据报的每次进行传输都要以UDP数据报DatagramPacket为基本单位。基础UDP 回显服务器Echo Sever功能客户端控制台输入字符串服务器原样返回。UDP 服务端父类 UdpEchoServerpackagenetwork;importjava.io.IOException;importjava.net.DatagramPacket;importjava.net.DatagramSocket;importjava.net.SocketException;importjava.nio.charset.StandardCharsets;//服务器publicclassUdpEchoServer{//创建一个 DatagramSocket 对象后续操作网卡的基础privateDatagramSocketsocketnull;publicUdpEchoServer(intport)throwsSocketException{socketnewDatagramSocket(port);}/* 服务器和客户端都需要创建 Socket 对象 1. 服务器的Socket一般要显示指定一个端口号此时Socket对象就能绑定到这个指定的端口 2. 客户端的Socket一般不能显示指定此时系统会自动分配一个随机的端口。 3. 服务器手动指定端口不会冲突因为一个服务器上都有哪些程序使用哪些端口都是可控的。但客户端不可控。 * */publicvoidstart(){//通过这个方法来启动服务器System.out.println(UDP回显服务器启动);//服务器7*24运行死循环持续处理请求while(true){//1.准备空缓冲区接收客户端数据包---读取请求并解析byte[]buffernewbyte[4096];DatagramPacketrequestPacketnewDatagramPacket(buffer,buffer.length);//阻塞等待客户端请求没有数据则线程挂起try{socket.receive(requestPacket);//由receive内部对数据进行填充数据以二进制形式存储到DatagramPacket中//2.解析数据包为字符串必须用实际收到长度getLength()StringrequestnewString(requestPacket.getData(),0,requestPacket.getLength());//3.业务处理回显逻辑交给process方法支持子类重写(一个服务器的核心步骤)Stringresponseprocess(request);//4.把响应写回客户端构造响应数据包携带客户端IP端口DatagramPacketresponsePacketnewDatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());//UDP 无连接自身不会保存数据要发送给谁就需要每次发送时重新指定数据要发到哪里去socket.send(responsePacket);//打印通信日志System.out.printf([%s:%d] req%s, resp%s%n,requestPacket.getAddress(),requestPacket.getPort(),request,response);}catch(IOExceptione){thrownewRuntimeException(e);}}}publicStringprocess(Strings){returns;}publicstaticvoidmain(String[]args)throwsSocketException{UdpEchoServerservernewUdpEchoServer(9090);server.start();}}思考Socket 也是文件为什么不写close也没出现文件泄露因为 Socket 是文件描述符表的一个表项每次打开一个文件就会占用一个位置文件描述符在PCB上跟随进程这个 Socket 在整个程序运行过程中都是需要使用的当它不再使用时就意味着程序要结束了进程结束随之文件描述符表就会销毁被系统自动回收。什么时候才会出现泄漏代码中频繁地打开文件但不关闭在一个进程的运行过程中不断积累打开的文件逐渐消耗掉文件描述符表中的内容最终消耗殆尽。若进程的生命周期很短则谈不上泄露。UDP 客户端 UdpEchoClientpackagenetwork;importjava.net.*;importjava.util.Scanner;publicclassUdpEchoClient{privateDatagramSocketsocketnull;privateStringserverIp ;privateintserverPort0;publicUdpEchoClient(Stringip,intport)throwsSocketException{//客户端不能手动指定端口系统自动分配随机空闲端口socketnewDatagramSocket();serverIpip;serverPortport;}publicvoidstart(){System.out.println(UDP客户端启动);ScannerscannernewScanner(System.in);while(true){//1.从控制台读取数据作为请求System.out.print(- );Stringrequestscanner.next();try{//2.构造请求数据包指定服务器IP端口DatagramPacketrequestPacketnewDatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(serverIp),serverPort);socket.send(requestPacket);//3.接收服务器响应byte[]buffernewbyte[4096];DatagramPacketresponsePacketnewDatagramPacket(buffer,buffer.length);socket.receive(responsePacket);//4.解析并打印响应StringresponsenewString(responsePacket.getData(),0,responsePacket.getLength());System.out.println(response);}catch(Exceptione){e.printStackTrace();}}}publicstaticvoidmain(String[]args)throwsSocketException{UdpEchoClientclientnewUdpEchoClient(127.0.0.1,9090);client.start();}}扩展继承实现 UDP 词典翻译服务器基于父类UdpEchoServer仅重写 process 方法通过 HashMap 实现英译汉字典查询。packagenetwork;importjava.net.SocketException;importjava.util.HashMap;importjava.util.Map;publicclassUdpDictServerextendsUdpEchoServer{privateMapString,StringdictnewHashMap();publicUdpDictServer(intport)throwsSocketException{super(port);//初始化词典dict.put(dog,小狗);dict.put(cat,小猫);dict.put(pig,小猪);}//重写process方法OverridepublicStringprocess(Strings){returndict.getOrDefault(s,词典中不存在该词语);}publicstaticvoidmain(String[]args)throwsSocketException{UdpDictServerservernewUdpDictServer(9090);server.start();}}TCP SocketTCP 是面向连接、面向字节流协议传输的基本单位是字节。ServerSocket给服务器使用的类使用这个类来绑定端口号。Socket既会给服务器使用也会给客户端使用。单线程 TCP 回显服务器TCP 服务端 TcpEchoServerimportjava.io.IOException;importjava.io.InputStream;importjava.io.OutputStream;importjava.io.PrintWriter;importjava.net.ServerSocket;importjava.net.Socket;importjava.util.Scanner;publicclassTcpEchoServer{privateServerSocketserverSocketnull;publicTcpEchoServer(intport)throwsIOException{serverSocketnewServerSocket(port);}publicvoidstart()throwsIOException{System.out.println(服务器启动);while(true){//阻塞等待客户端连接连接建立由内核三次握手自动完成完成后会在内核队列中排队SocketclientsocketserverSocket.accept();//处理当前客户端连接processConnection(clientsocket);}}//处理单个客户端完整通信逻辑privatevoidprocessConnection(SocketclientSocket){//打印日志表示当前有客户端连上了System.out.printf([%s:%d] 客户端上线%n,clientSocket.getInetAddress(),clientSocket.getPort());try(InputStreaminputStreamclientSocket.getInputStream();OutputStreamoutputStreamclientSocket.getOutputStream()){/* InputStream 和 OutputStream 就是字节流 通过 InputStream 进行 read 操作---接收 通过 OutputStream 进行 write 操作---发送 * */ScannerscannernewScanner(inputStream);PrintWriterprintWriternewPrintWriter(outputStream);while(true){if(!scanner.hasNext()){//没有数据/连接断开此时循环结束System.out.printf([%s:%d] 客户端下线%n,clientSocket.getInetAddress(),clientSocket.getPort());break;}//1.读取请求(以空白符、换行作为分隔)Stringrequestscanner.next();//2.根据请求计算响应Stringresponseprocess(request);//3.把响应写回到客户端printWriter.println(response);//刷新缓冲区强制把内存数据写入网卡printWriter.flush();System.out.printf([%s:%d] req%s, resp%s%n,clientSocket.getInetAddress(),clientSocket.getPort(),request,response);}}catch(IOExceptione){thrownewRuntimeException(e);}finally{// 确保连接断开时关闭Socket释放文件描述符// 这里需要close是因为当前方法processConnection就是在处理一个连接当该方法结束时连接要断开clientSocket在循环中每次有一个新的客户端进来建立连接都会创建出新的clientSocket//DatagramSocketServerSocket在程序中只有一个对象不close也没事try{clientSocket.close();}catch(IOExceptione){e.printStackTrace();}}}publicStringprocess(Stringrequest){returnrequest;}publicstaticvoidmain(String[]args)throwsIOException{TcpEchoServerservernewTcpEchoServer(9090);server.start();}}TCP 客户端 TcpEchoClientimportjava.io.IOException;importjava.io.InputStream;importjava.io.OutputStream;importjava.io.PrintWriter;importjava.net.Socket;importjava.util.Scanner;publicclassTcpEchoClient{privateSocketsocketnull;//在创建Socket的同时和服务器建立连接若连接建立失败则对象new失败publicTcpEchoClient(StringserverIp,intserverPort)throwsIOException{// new Socket时内核自动发起三次握手建立TCP连接socketnewSocket(serverIp,serverPort);}publicvoidstart(){ScannerscannernewScanner(System.in);try(InputStreaminputStreamsocket.getInputStream();OutputStreamoutputStreamsocket.getOutputStream()){PrintWriterprintWriternewPrintWriter(outputStream);ScannerscannerNextworknewScanner(inputStream);while(true){System.out.print(- );Stringrequestscanner.next();//发送请求printWriter.println(request);printWriter.flush();//读取响应StringresponsescannerNextwork.next();System.out.println(response);}}catch(IOExceptione){thrownewRuntimeException(e);}}publicstaticvoidmain(String[]args)throwsIOException{TcpEchoClientclientnewTcpEchoClient(127.0.0.1,9090);client.start();}}单线程 TCP 服务器致命缺陷无法并发处理多客户端单线程服务器start()方法只有一个主线程主线程调用accept()拿到第一个客户端连接后进入processConnection内部死循环如果第一个客户端一直不关闭连接、持续占用循环主线程会阻塞在scanner.hasNext()第二个客户端可以和服务器建立 TCP 连接内核完成三次握手与代码无关连接会存入内核等待队列但应用层永远无法调用accept()取出连接第二个客户端完全无法通信。优化方案一多线程 TCP 服务器每接收到一个客户端连接创建一个独立新线程处理通信逻辑主线程立刻回到accept()继续监听新连接实现并发。修改start():publicvoidstart()throwsIOException{System.out.println(TCP多线程服务器启动);while(true){SocketclientSocketserverSocket.accept();// 新建线程处理连接主线程非阻塞立刻循环下一次acceptnewThread(()-processConnection(clientSocket)).start();}}缺点高并发场景下频繁创建、销毁线程会带来巨大开销极端情况引发服务器卡顿。因此可以使用线程池优化。优化方案二线程池 TCP 服务器使用CachedThreadPool缓存线程复用空闲线程避免频繁创建销毁线程是生产环境基础方案。importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;publicvoidstart()throwsIOException{System.out.println(TCP线程池服务器启动);// 可缓存线程池自动扩容、复用空闲线程ExecutorServicepoolExecutors.newCachedThreadPool();while(true){SocketclientSocketserverSocket.accept();// 提交任务到线程池pool.submit(()-processConnection(clientSocket));}}总结UDP 数据包解析必须使用getLength()不能用getBytes().length字节数组缓冲区有多余空位getLength()代表本次实际接收的字节长度字节与字符串长度区分str.length()是字符个数str.getBytes()是字节个数包含中文时二者不一致网络传输必须统一使用字节TCP PrintWriter 必须 flush ()IO 缓冲区会积攒数据不刷新缓冲区数据不会真正发送到网卡TCP ClientSocket 必须 finally 关闭循环创建客户端连接不手动关闭会持续占用文件描述符引发泄漏单线程 TCP 天然不支持并发外层 accept 循环和内层读写循环在同一个线程必须通过多线程 / 线程池解耦UDP 无连接数据包必须携带完整目的地址TCP 面向连接连接建立后全程只需要读写流无需重复携带地址。本篇是「Java后端编程系列」的连载内容点击链接查看完整系列 上一篇网络初识基础入门局域网广域网、IP 端口、TCP/IP 五层模型与封装解封装全过程 下一篇网络三彻底搞懂网络底层五层模型、序列化、MAC/ARP、MTU 一次吃透 点击直达 Java后端基础专栏合集