【Java】TCP网络编程:从可靠传输到Socket实战
Java 中的 TCP 网络编程是后端开发最基础、最重要的技能之一。它基于TCP/IP 协议栈的传输层 TCP,提供面向连接、可靠、有序、流量控制、拥塞控制的字节流传输。
本文从 TCP 的可靠传输核心机制讲起,一步步带你理解为什么 TCP 可靠,然后通过 Java Socket 实战代码,逐步实现从简单 echo 到多线程聊天室的完整过程。
1. TCP 为什么是“可靠传输”?核心机制一览
TCP 之所以被称为可靠传输协议,靠的是以下几大机制:
| 机制 | 作用 | 实现方式 |
|---|---|---|
| 三次握手 | 建立可靠连接,确保双方都能收发数据 | SYN → SYN-ACK → ACK |
| 序列号 + 确认应答 | 保证数据有序到达,丢失/乱序可重传 | 每个字节都有 seq,接收方回复 ack |
| 超时重传 | 发送方超时未收到 ACK 则重传 | RTO(重传超时时间)动态计算 |
| 滑动窗口 | 实现流量控制,避免快发慢收 | 接收窗口(advertised window) |
| 拥塞控制 | 避免网络拥塞(慢启动、拥塞避免、快速重传、快速恢复) | 拥塞窗口(cwnd)动态调整 |
| 校验和 | 检测数据是否损坏 | TCP 头部 + 数据部分的 checksum |
| 四次挥手 | 安全关闭连接,确保双方数据都已确认 | FIN → ACK → FIN → ACK |
三次握手简图(建立连接):
客户端 服务端 | SYN (seq=x) | |------------------>| | SYN-ACK (seq=y, ack=x+1) | |<------------------| | ACK (ack=y+1) | |------------------>| 连接建立(全双工)四次挥手简图(关闭连接):
一方 另一方 | FIN (seq=m) | |------------------>| | ACK (ack=m+1) | |<------------------| | (可能继续发数据)| | FIN (seq=n) | |<------------------| | ACK (ack=n+1) | |------------------>| 连接完全关闭Java 层面:你不需要手动实现这些机制。
当你调用new Socket(host, port)时,操作系统内核的 TCP 协议栈自动完成三次握手;
调用socket.close()时自动发起四次挥手。
2. Java TCP 编程核心类
| 类名 | 作用 | 常用构造方法 / 方法 |
|---|---|---|
ServerSocket | 服务端监听套接字 | new ServerSocket(port)accept() |
Socket | 客户端 / 已连接的客户端套接字 | new Socket(host, port)getInputStream()getOutputStream() |
InputStream | 从 Socket 读字节流 | BufferedReader/DataInputStream |
OutputStream | 向 Socket 写字节流 | PrintWriter/DataOutputStream |
注意:Socket 的流是全双工的,可以同时读写。
3. 实战一:最简单的单线程 Echo 服务器 & 客户端
服务器端(EchoServer.java)
importjava.io.*;importjava.net.*;publicclassEchoServer{publicstaticvoidmain(String[]args){intport=8888;try(ServerSocketserverSocket=newServerSocket(port)){System.out.println("Echo Server 启动,监听端口:"+port);while(true){// 阻塞等待客户端连接(三次握手在这里完成)SocketclientSocket=serverSocket.accept();System.out.println("客户端连接成功:"+clientSocket.getInetAddress());// 获取输入输出流try(BufferedReaderin=newBufferedReader(newInputStreamReader(clientSocket.getInputStream()));PrintWriterout=newPrintWriter(clientSocket.getOutputStream(),true)){Stringline;while((line=in.readLine())!=null){System.out.println("收到:"+line);out.println("Echo: "+line);// 回显out.flush();}}catch(IOExceptione){System.out.println("客户端断开:"+e.getMessage());}}}catch(IOExceptione){e.printStackTrace();}}}客户端(EchoClient.java)
importjava.io.*;importjava.net.*;importjava.util.Scanner;publicclassEchoClient{publicstaticvoidmain(String[]args){Stringhost="localhost";intport=8888;try(Socketsocket=newSocket(host,port);PrintWriterout=newPrintWriter(socket.getOutputStream(),true);BufferedReaderin=newBufferedReader(newInputStreamReader(socket.getInputStream()));Scannerscanner=newScanner(System.in)){System.out.println("已连接到服务器 "+host+":"+port);System.out.println("输入消息(输入 quit 退出):");StringuserInput;while(!(userInput=scanner.nextLine()).equals("quit")){out.println(userInput);out.flush();Stringresponse=in.readLine();System.out.println("服务器回复:"+response);}}catch(IOExceptione){e.printStackTrace();}}}运行顺序:先运行 EchoServer,再运行多个 EchoClient 窗口测试。
4. 实战二:多线程版本聊天室(支持多个客户端)
服务器端(ChatServer.java)
importjava.io.*;importjava.net.*;importjava.util.*;publicclassChatServer{privatestaticfinalintPORT=9999;privatestaticfinalList<ClientHandler>clients=newArrayList<>();publicstaticvoidmain(String[]args)throwsIOException{ServerSocketserverSocket=newServerSocket(PORT);System.out.println("聊天室服务器启动,端口:"+PORT);while(true){SocketclientSocket=serverSocket.accept();System.out.println("新用户加入:"+clientSocket.getInetAddress());ClientHandlerhandler=newClientHandler(clientSocket);synchronized(clients){clients.add(handler);}newThread(handler).start();}}// 广播消息给所有客户端publicstaticvoidbroadcast(Stringmessage,ClientHandlersender){synchronized(clients){for(ClientHandlerclient:clients){if(client!=sender){client.sendMessage(message);}}}}// 移除断开客户端publicstaticvoidremoveClient(ClientHandlerclient){synchronized(clients){clients.remove(client);}}staticclassClientHandlerimplementsRunnable{privatefinalSocketsocket;privatePrintWriterout;privateStringusername;publicClientHandler(Socketsocket){this.socket=socket;}@Overridepublicvoidrun(){try(BufferedReaderin=newBufferedReader(newInputStreamReader(socket.getInputStream()));PrintWriterwriter=newPrintWriter(socket.getOutputStream(),true)){this.out=writer;// 读取用户名username=in.readLine();System.out.println(username+" 加入聊天室");broadcast(username+" 加入了聊天室",this);Stringmessage;while((message=in.readLine())!=null){if("quit".equalsIgnoreCase(message)){break;}Stringformatted=username+" : "+message;System.out.println(formatted);broadcast(formatted,this);}}catch(IOExceptione){// 客户端异常断开}finally{System.out.println(username+" 离开聊天室");broadcast(username+" 离开了聊天室",this);ChatServer.removeClient(this);try{socket.close();}catch(IOExceptionignored){}}}publicvoidsendMessage(Stringmsg){if(out!=null){out.println(msg);out.flush();}}}}客户端(ChatClient.java)与上面类似,修改为:
- 先输入用户名
- 循环发送消息,支持
quit退出
5. 常见问题与最佳实践
- 粘包 / 半包:TCP 是字节流,无边界。解决方法:
- 定长协议
- 特殊分隔符(如
\n) - 长度前缀(最推荐)
- 优雅关闭:
socket.shutdownOutput()→ 半关闭;再读到-1再完全关闭 - 心跳机制:定期发送 ping/pong 检测连接是否存活
- 线程模型:
- 单线程 → 简单场景
- 线程池 + NIO → 高并发(Netty 更优)
- 异常处理:
SocketException、IOException要仔细捕获 - 资源释放:用 try-with-resources 自动关闭流和 socket
总结:一句话记住 TCP + Socket
TCP 提供了可靠的管道,Java 的 Socket 只是这根管道的两端把手。你只需要关心“怎么读写数据”,内核帮你完成了三次握手、序列号、重传、拥塞控制等所有复杂工作。
接下来想深入哪个方向?
- 解决粘包半包的长度前缀协议实战
- 使用线程池优化多客户端
- 引入 NIO / Netty 的对比
- 实现心跳 + 重连机制
告诉我,我可以继续带你写更完整的代码!