【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)
import java.io.*;
import java.net.*;
public class EchoServer {
public static void main(String[] args) {
int port = 8888;
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("Echo Server 启动,监听端口:" + port);
while (true) {
// 阻塞等待客户端连接(三次握手在这里完成)
Socket clientSocket = serverSocket.accept();
System.out.println("客户端连接成功:" + clientSocket.getInetAddress());
// 获取输入输出流
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
String line;
while ((line = in.readLine()) != null) {
System.out.println("收到:" + line);
out.println("Echo: " + line); // 回显
out.flush();
}
} catch (IOException e) {
System.out.println("客户端断开:" + e.getMessage());
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端(EchoClient.java)
import java.io.*;
import java.net.*;
import java.util.Scanner;
public class EchoClient {
public static void main(String[] args) {
String host = "localhost";
int port = 8888;
try (Socket socket = new Socket(host, port);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
Scanner scanner = new Scanner(System.in)) {
System.out.println("已连接到服务器 " + host + ":" + port);
System.out.println("输入消息(输入 quit 退出):");
String userInput;
while (!(userInput = scanner.nextLine()).equals("quit")) {
out.println(userInput);
out.flush();
String response = in.readLine();
System.out.println("服务器回复:" + response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行顺序:先运行 EchoServer,再运行多个 EchoClient 窗口测试。
4. 实战二:多线程版本聊天室(支持多个客户端)
服务器端(ChatServer.java)
import java.io.*;
import java.net.*;
import java.util.*;
public class ChatServer {
private static final int PORT = 9999;
private static final List<ClientHandler> clients = new ArrayList<>();
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(PORT);
System.out.println("聊天室服务器启动,端口:" + PORT);
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("新用户加入:" + clientSocket.getInetAddress());
ClientHandler handler = new ClientHandler(clientSocket);
synchronized (clients) {
clients.add(handler);
}
new Thread(handler).start();
}
}
// 广播消息给所有客户端
public static void broadcast(String message, ClientHandler sender) {
synchronized (clients) {
for (ClientHandler client : clients) {
if (client != sender) {
client.sendMessage(message);
}
}
}
}
// 移除断开客户端
public static void removeClient(ClientHandler client) {
synchronized (clients) {
clients.remove(client);
}
}
static class ClientHandler implements Runnable {
private final Socket socket;
private PrintWriter out;
private String username;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter writer = new PrintWriter(socket.getOutputStream(), true)) {
this.out = writer;
// 读取用户名
username = in.readLine();
System.out.println(username + " 加入聊天室");
broadcast(username + " 加入了聊天室", this);
String message;
while ((message = in.readLine()) != null) {
if ("quit".equalsIgnoreCase(message)) {
break;
}
String formatted = username + " : " + message;
System.out.println(formatted);
broadcast(formatted, this);
}
} catch (IOException e) {
// 客户端异常断开
} finally {
System.out.println(username + " 离开聊天室");
broadcast(username + " 离开了聊天室", this);
ChatServer.removeClient(this);
try {
socket.close();
} catch (IOException ignored) {}
}
}
public void sendMessage(String msg) {
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 的对比
- 实现心跳 + 重连机制
告诉我,我可以继续带你写更完整的代码!