【Java SE】网络编程套接字 文章目录一、Socket套接字1. 什么是Socket2. Socket中两个核心协议1流套接字基于TCP协议2数据报套接字基于UDP协议二、UDP数据报套接字编程1. 核心API1DatagramSocketUDP套接字2DatagramPacketUDP数据报3InetSocketAddress2.UDP回显服务器Echo Server服务端代码UdpEchoServer.java客户端代码UdpEchoClient.java三、TCP流套接字编程1. 核心API介绍1ServerSocketTCP服务端套接字2SocketTCP通信套接字2. TCP回显服务器服务端代码TcpEchoServer.java客户端代码TcpEchoClient.java3. TCP服务端支持多客户端线程池优化多线程版服务端四、TCP长短连接短连接长连接一、Socket套接字1. 什么是SocketSocket套接字是系统提供的网络通信技术是基于TCP/IP协议的网络编程基本操作单元。Socket就像通信端口进程通过这个端口发送和接收数据实现跨设备/跨进程通信。2. Socket中两个核心协议1流套接字基于TCP协议有连接通信前必须建立连接可靠传输数据不会丢失、重复且按发送顺序到达面向字节流以字节为单位读取数据这样就支持任意长度但也带来了“粘包问题”。数据无边界可多次发送、分多次接收双缓冲区有发送缓冲区和接收缓冲区数据先存入缓冲区再传输大小不限可传输任意大小的数据。2数据报套接字基于UDP协议无连接通信前无需建立连接直接发送数据不可靠传输数据可能丢失、重复或乱序面向数据报以一个数据报为单位一次必须读写一个数据报而不能是半个仅接收缓冲区无发送缓冲区数据直接发送大小受限一次最多传输64KB数据。二、UDP数据报套接字编程UDP是无连接协议编程流程相对简单核心类为DatagramSocket套接字和DatagramPacket数据报。1. 核心API1DatagramSocketUDP套接字用于发送和接收UDP数据报关键构造方法和方法构造方法/方法说明DatagramSocket()创建客户端套接字绑定本机随机端口DatagramSocket(int port)创建服务端套接字绑定本机指定端口void receive(DatagramPacket p)阻塞接收数据报无数据则等待void send(DatagramPacket p)发送数据报无需等待直接发送void close()关闭套接字释放资源2DatagramPacketUDP数据报封装UDP传输的数据、目标IP和端口关键构造方法和方法构造方法/方法说明DatagramPacket(byte[] buf, int length)用于接收数据数据存入buf数组接收长度为lengthDatagramPacket(byte[] buf, int offset, int length, SocketAddress address)用于发送数据buf为发送数据address指定目标IP和端口InetAddress getAddress()获取发送端/接收端IP地址int getPort()获取发送端/接收端端口号byte[] getData()获取数据报中的数据3InetSocketAddress创建包含IP和端口的Socket地址构造方法InetSocketAddress(InetAddress addr, int port)。2.UDP回显服务器Echo Server服务端代码UdpEchoServer.javaimportjava.io.IOException;importjava.net.DatagramPacket;importjava.net.DatagramSocket;importjava.net.SocketException;publicclassUDPEchoServer{privateDatagramSocketsocketnull;//创建socket对象代表网卡文件//读这个文件就等于从网卡中收数据写这个文件就等于让网卡发数据publicUDPEchoServer(intport)throwsSocketException{socketnewDatagramSocket(port);//指定端口号}publicvoidstart()throwsIOException{System.out.println(服务器启动);while(true){//循环一次相当于一次请求//处理请求时典型的服务器分为三个步骤//1、读取请求并解析// DatagramPacket 相当于一个UDP数据报此处传入字节数组就保存UDP数据报的载荷部分// 其中包括报头载荷这里通过 new 字节数组来保存DatagramPacketrequestPacketnewDatagramPacket(newbyte[4096],4096);socket.receive(requestPacket);//输出型参数// 把UDP数据包载荷二进制数据取出来转为字符串// 1通过requestPacket.getData()拿到DatagramPacket中的字节数组// 2通过requestPacket.getLength()拿到有效数据的长度// 3根据字节数组构造出一个StringStringrequestnewString(requestPacket.getData(),0,requestPacket.getLength());//2、根据请求计算相应也是服务器的核心逻辑Stringresponseprocess(request);//3、把相应返回给客户端// 根据response构造DatagramPacket这里是响应数据报发送客户端// 1通过response.getBytes()拿到字节数组// 2response.getBytes().length拿到字节数组长度// 此处不能使用response.length因为response.length拿到的是字符数组的长度单位是字符// 3 requestPacket.getSocketAddress()拿到客户端的IP和端口号数据从哪里来就回到哪里去DatagramPacketresponsePacketnewDatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());//依据responsePacket中的IP和目的端口发送数据socket.send(responsePacket);//4、打印日志 记录客户端/服务端交互过程System.out.printf([%s:%d] req: %s, resp: %s\n,requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);}}//此处是回显逻辑请求响应publicStringprocess(Stringrequest){returnrequest;}staticvoidmain(){}}思考socket是否需要关闭这需要从socket的生命周期来考虑问题这里的socket对象自始至终伴随UDP服务器如果服务器关闭进程结束进程结束时会自动释放PCB的文件描述符表中所有资源也就不需要手动close了在客户端请求过来之前服务器都在干啥recieve会触发阻塞行为直到客户端的请求发来recieve才会返回客户端代码UdpEchoClient.javaimportjava.io.IOException;importjava.net.*;importjava.util.Scanner;publicclassUDPEchoClient{privateDatagramSocketsocket;//UDP本身不保存对端信息需要在代码中保存privateStringserverIp;privateintserverPort;//在构造函数中指定访问的服务器地址publicUDPEchoClient(StringserverIp,intserverPort)throwsSocketException{this.serverIpserverIp;this.serverPortserverPort;//这里必须使用无参的构造方法由操作系统分配空闲端口socketnewDatagramSocket();}publicvoidstart()throwsIOException{ScannerscannewScanner(System.in);while(true){//1.从控制台读取System.out.println(请输入要发送的内容);Stringrequestscan.next();//2.把请求发送给服务器// 构造过程不光要构造载荷也要设置服务器IP和端口号// IP是通过字符串来构造的通过InetAddress.getByName(serverIp)把字符串转为Java标注库可识别的对象DatagramPacketrequestPacketnewDatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(serverIp),serverPort);//3.发送数据报socket.send(requestPacket);//4.接受服务器响应DatagramPacketresponsePacketnewDatagramPacket(newbyte[1024],1024);socket.receive(responsePacket);//5.从服务器中读取数据进行解析并打印StringresponsenewString(responsePacket.getData(),0,responsePacket.getLength());System.out.println(response);}}staticvoidmain()throwsIOException{UDPEchoClientclientnewUDPEchoClient(127.0.0.1,9090);client.start();}}与服务器的逻辑相同客户端的socket跟随整个进程没有必要手动释放资源三、TCP流套接字编程TCP是有连接协议通信前需建立连接三次握手通信后需关闭连接四次挥手可靠性更高适合对数据传输稳定性要求高的场景文件传输、登录验证。1. 核心API介绍1ServerSocketTCP服务端套接字用于创建TCP服务端监听客户端连接关键构造方法和方法构造方法/方法说明ServerSocket(int port)创建服务端套接字绑定指定端口Socket accept()阻塞监听客户端连接连接成功返回与客户端通信的Socketvoid close()关闭服务端套接字2SocketTCP通信套接字客户端套接字或服务端accept方法返回的通信套接字用于收发数据关键构造方法和方法构造方法/方法说明Socket(String host, int port)客户端创建套接字与指定IP和端口的服务端建立连接InputStream getInputStream()获取输入流读取对端数据OutputStream getOutputStream()获取输出流向对端发送数据InetAddress getInetAddress()获取对端IP地址void close()关闭套接字释放连接2. TCP回显服务器TCP通信流程服务端监听端口→客户端连接→建立连接后收发数据→关闭连接。服务端代码TcpEchoServer.javaimportjava.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){//与UDP不同TCP需要先处理客户端发来的连接//accept类似于“接通电话”的操作//通过读写 clientSocket来和客户端进行通信SocketclientSocketserverSocket.accept();processConnection(clientSocket);}}privatevoidprocessConnection(SocketclientSocket)throwsIOException{System.out.printf([%s,%d] 客户端上线,clientSocket.getInetAddress(),clientSocket.getPort());try(InputStreaminputStreamclientSocket.getInputStream();OutputStreamoutputStreamclientSocket.getOutputStream()){//针对InputStream套了一层ScannerscannewScanner(inputStream);//针对OutputStream套了一层PrintWriterwriternewPrintWriter(outputStream);while(true){//1.读取请求并解析if(!scan.hasNext()){//没有下一条数据说明连接断开System.out.printf([%s:%d],clientSocket.getInetAddress(),clientSocket.getPort());break;}Stringrequestscan.next();//2.根据请求计算响应Stringresponseprocess(request);//3.返回响应到客户端//println这个操作只是把数据放进发送缓冲区内存中并没有真正地写入网卡writer.println(response);writer.flush();//4.打印日志System.out.printf([%s:%d] req:%s,resp:%s,clientSocket.getInetAddress(),clientSocket.getPort(),request,response);}}catch(IOExceptione){thrownewIOException(e);}}privateStringprocess(Stringrequest){returnrequest;}publicstaticvoidmain(String[]args)throwsIOException{TCPEchoServerservernewTCPEchoServer(9090);server.start();}}客户端代码TcpEchoClient.javaimportjava.io.IOException;importjava.io.InputStream;importjava.io.OutputStream;importjava.io.PrintWriter;importjava.net.Socket;importjava.util.Scanner;publicclassTCPEchoClient{SocketclientSocketnull;TCPEchoClient(StringserverIP,intserverPort)throwsIOException{//TCP创建socket对象时就会在底层和对端建立TCP的连接记录对端信息//IP和端口这样的信息不需要自己创建变量保存SocketsocketnewSocket(serverIP,serverPort);}voidstart(){ScannerscannewScanner(System.in);try(InputStreaminputStreamclientSocket.getInputStream();OutputStreamoutputStreamclientSocket.getOutputStream()){ScannerscannerNetnewScanner(inputStream);PrintWriterwriternewPrintWriter(outputStream);while(true){//1.从控制台读取用户输入Stringrequestscan.next();//2.发送给服务器//println这个操作只是把数据放进发送缓冲区内存中并没有真正地写入网卡writer.println(request);writer.flush();//3.读取服务器给出的响应StringresponsescannerNet.next();//4.打印到控制台System.out.println(response);}}catch(IOExceptione){thrownewRuntimeException(e);}}}注意服务端中使用的serverSocket与客户端使用的socket对象绝对不是同一个对象但是存在一定关联。这两个socket可以理解为两部电话从A话筒说话那么B的话筒也可以听到。服务端通过scan.hasNext()来判断是否有下一条数据就是通过判断是否包括“空白符”换行回车空格制表符翻页符…。遇到空白符才认为是完整的next否则会一直阻塞。因此客户端发送数据时writer.println(request)必须使用prinln函数而不能使用print函数。这样就暗中约定好一个请求/响应使用\n作为结束标记对端读取时读到\n结束。服务端中的clientSocket只对应一个客户端的请求当一个客户端请求结束时它的生命周期也就结束了此时需要手动释放。3. TCP服务端支持多客户端线程池优化服务端存在一个问题当连接客户端1而客户端1没有发送请求服务端会一直阻塞在hasNext()这也意味着accept()不能被执行除非客户端1断开连接。这说明当前代码无法同时等待accept和用户请求。解决方案是为每个客户端分配独立线程或使用线程池减少线程创建销毁的开销。多线程版服务端importjava.io.IOException;importjava.io.InputStream;importjava.io.OutputStream;importjava.io.PrintWriter;importjava.lang.reflect.Executable;importjava.net.ServerSocket;importjava.net.Socket;importjava.util.Scanner;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;publicclassTCPEchoServer{privateServerSocketserverSocketnull;publicTCPEchoServer(intport)throwsIOException{serverSocketnewServerSocket(port);}publicvoidstart()throwsIOException{System.out.println(服务器启动);//这种情况不使用fixedThreadPool,因为会固定客户端连接数目ExecutorServiceexecutorServiceExecutors.newCachedThreadPool();while(true){//与UDP不同TCP需要先处理客户端发来的连接//accept类似于“接通电话”的操作//通过读写 clientSocket来和客户端进行通信// 主线程负责进行 accept, 每次 accept 到一个客户端, 就创建一个线程, 由新线程负责处理客户端的请求.SocketclientSocketserverSocket.accept();//使用多线程优化// Thread t new Thread(()-{// try {// processConnection(clientSocket);// } catch (IOException e) {// throw new RuntimeException(e);// }// });// t.start();//使用线程池优化executorService.submit(()-{try{processConnection(clientSocket);}catch(IOExceptione){thrownewRuntimeException(e);}});}}privatevoidprocessConnection(SocketclientSocket)throwsIOException{System.out.printf([%s,%d] 客户端上线\n,clientSocket.getInetAddress(),clientSocket.getPort());try(InputStreaminputStreamclientSocket.getInputStream();OutputStreamoutputStreamclientSocket.getOutputStream()){//针对InputStream套了一层ScannerscannewScanner(inputStream);//针对OutputStream套了一层PrintWriterwriternewPrintWriter(outputStream);while(true){//1.读取请求并解析if(!scan.hasNext()){//没有下一条数据说明连接断开System.out.printf([%s:%d]客户端下线\n,clientSocket.getInetAddress(),clientSocket.getPort());break;}Stringrequestscan.next();//2.根据请求计算响应Stringresponseprocess(request);//3.返回响应到客户端writer.println(response);writer.flush();//4.打印日志System.out.printf([%s:%d] req:%s,resp:%s \n,clientSocket.getInetAddress(),clientSocket.getPort(),request,response);}}catch(IOExceptione){thrownewIOException(e);}finally{clientSocket.close();}}privateStringprocess(Stringrequest){returnrequest;}publicstaticvoidmain(String[]args)throwsIOException{TCPEchoServerservernewTCPEchoServer(9090);server.start();}}四、TCP长短连接TCP连接的生命周期决定了是长连接还是短连接两者适用场景不同短连接逻辑每次请求-响应后立即关闭连接特点每次通信都要建立/关闭连接耗时略高适用场景客户端请求频率低的场景例如浏览网页、查询天气。长连接逻辑建立连接后不关闭持续收发数据特点仅第一次建立连接后续通信效率高支持服务端主动推送数据适用场景通信频繁的场景例如聊天室、实时游戏、股票行情。注意基于BIO同步阻塞IO的长连接会占用大量线程资源高并发场景下建议使用NIO同步非阻塞IO实现长连接提升性能。端口被占用问题如果启动服务端时出现java.net.BindException: Address already in use说明端口已被其他进程占用解决方案关闭占用端口的进程通过netstat -ano | findstr 端口号Windows或lsof -i:端口号Linux/Mac查找进程PID再通过任务管理器/kill命令关闭修改服务端绑定的端口使用未被占用的端口。