TCP Socket 编程实践
理论学了这么多,动手写代码才能真正理解。本篇用 Java Socket 实现一个完整的 TCP 客户端-服务端通信程序。
1. Socket 是什么?
Socket(套接字) 是操作系统提供的网络编程接口,是应用层与传输层之间的"门"。应用程序通过 Socket 发送和接收数据,不需要关心底层 TCP/IP 的实现细节。
应用层代码
↕ write() / read()
Socket(套接字)
↕
TCP / UDP(传输层)
↕
IP(网络层)
↕
网卡 → 网络
一个 TCP Socket 由五元组标识:
{协议(TCP), 本地IP, 本地端口, 远程IP, 远程端口}
2. 核心 API 速查
2.1 服务端 API
| 类/方法 | 作用 |
|---|---|
ServerSocket(int port) | 在指定端口创建服务端 Socket,开始监听 |
socket.accept() | 阻塞等待客户端连接,返回一个 Socket 对象 |
socket.getInputStream() | 获取输入流,读取客户端数据 |
socket.getOutputStream() | 获取输出流,向客户端发送数据 |
socket.close() | 关闭连接 |
2.2 客户端 API
| 方法 | 作用 |
|---|---|
new Socket(host, port) | 向服务端发起 TCP 连接(触发三次握手) |
socket.getInputStream() | 获取输入流,读取服务端数据 |
socket.getOutputStream() | 获取输出流,向服务端发送数据 |
socket.close() | 关闭连接(触发四次挥手) |
3. 最简示例:Echo 服务器
服务端收到什么就原样返回。
3.1 服务端
// 服务端:在 8080 端口监听,收到消息原样返回
public class EchoServer {
public static void main(String[] args) throws Exception {
// 1. 创建 ServerSocket,监听 8080 端口
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("服务端启动,等待连接...");
// 2. 阻塞等待客户端连接(三次握手在这里完成)
Socket socket = serverSocket.accept();
System.out.println("客户端已连接:" + socket.getRemoteSocketAddress());
// 3. 获取输入输出流
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
// 4. 读取客户端消息,原样返回
String message;
while ((message = in.readLine()) != null) {
System.out.println("收到: " + message);
out.println("Echo: " + message); // 原样返回
}
// 5. 关闭
socket.close();
serverSocket.close();
}
}
3.2 客户端
// 客户端:连接服务端,发送消息,接收回复
public class EchoClient {
public static void main(String[] args) throws Exception {
// 1. 连接服务端(触发三次握手)
Socket socket = new Socket("127.0.0.1", 8080);
System.out.println("已连接服务端");
// 2. 获取输入输出流
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
// 3. 从控制台读取输入,发送给服务端
BufferedReader console = new BufferedReader(
new InputStreamReader(System.in));
String userInput;
while ((userInput = console.readLine()) != null) {
out.println(userInput); // 发送
String response = in.readLine(); // 接收回复
System.out.println("服务端回复: " + response);
}
// 4. 关闭(触发四次挥手)
socket.close();
}
}
3.3 运行效果
# 服务端输出
服务端启动,等待连接...
客户端已连接:/127.0.0.1:54321
收到: Hello
收到: TCP编程不难
# 客户端输出
已连接服务端
Hello ← 用户输入
服务端回复: Echo: Hello
TCP编程不难 ← 用户输入
服务端回复: Echo: TCP编程不难
4. 进阶:多线程服务端
上面的 Echo 服务器只能处理一个客户端。实际应用中,服务端需要同时处理多个客户端——每来一个连接,就开一个线程处理。
// 多线程服务端:每个客户端连接由独立线程处理
public class MultiThreadServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("多线程服务端启动,等待连接...");
while (true) {
// 主线程循环:不断接受新连接
Socket socket = serverSocket.accept();
System.out.println("新客户端连接:" + socket.getRemoteSocketAddress());
// 每个连接分配一个线程处理
new Thread(() -> handleClient(socket)).start();
}
}
// 处理单个客户端的通信
private static void handleClient(Socket socket) {
try (
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
) {
String message;
while ((message = in.readLine()) != null) {
System.out.println(Thread.currentThread().getName()
+ " 收到: " + message);
out.println("Echo: " + message);
}
} catch (Exception e) {
System.out.println("客户端断开:" + socket.getRemoteSocketAddress());
} finally {
try { socket.close(); } catch (Exception ignored) {}
}
}
}
5. 编程中的 TCP 细节
5.1 连接超时
客户端连接服务端时,如果服务端不可达,new Socket() 会阻塞很长时间(默认可能几十秒)。可以设置超时:
Socket socket = new Socket();
socket.connect(new InetSocketAddress("192.168.1.100", 8080), 3000); // 3 秒超时
// 如果 3 秒内连不上,抛出 SocketTimeoutException
5.2 读写超时
读取数据时也可以设置超时,避免长时间阻塞:
socket.setSoTimeout(5000); // 5 秒读超时
// 如果 5 秒内没收到数据,read() 抛出 SocketTimeoutException
5.3 TCP_NODELAY:关闭 Nagle 算法
默认开启 Nagle 算法(攒够一批数据再发)。对于实时性要求高的场景(游戏、即时通讯),可以关闭:
socket.setTcpNoDelay(true); // 关闭 Nagle,每次 send 立即发出
5.4 SO_REUSEADDR:端口复用
服务端重启时,可能遇到"端口被占用"(TIME_WAIT 状态)。设置端口复用可以解决:
ServerSocket serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true); // 允许复用处于 TIME_WAIT 的端口
serverSocket.bind(new InetSocketAddress(8080));
5.5 优雅关闭(半关闭)
TCP 支持半关闭——一方关闭输出(不再发数据),但仍然可以接收数据:
socket.shutdownOutput(); // 关闭输出流(发送 FIN),但输入流仍可读
// 对方会收到 EOF(read 返回 -1),但对方仍然可以发数据过来
// 继续读取对方数据...
String msg = in.readLine();
socket.close(); // 最终关闭
6. Socket 编程常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
ConnectException: Connection refused | 服务端未启动或端口错误 | 检查服务端是否启动、端口是否一致 |
SocketTimeoutException: connect timed out | 网络不通或防火墙拦截 | 检查网络连通性、防火墙规则 |
EOFException / read 返回 -1 | 对方关闭了连接 | 正常处理连接断开逻辑 |
BindException: Address already in use | 端口被占用或 TIME_WAIT | 使用 SO_REUSEADDR 或等待 TIME_WAIT 过期 |
| 读取阻塞死等 | 对方未发送数据 | 设置 SO_TIMEOUT |
| 乱码 | 编码不一致 | 统一使用 UTF-8 |
7. 小结
Socket 编程是 TCP 理论的实践落地。核心流程就三步:建立连接 → 读写数据 → 关闭连接。多线程是服务端处理并发连接的基础方案(生产环境通常用线程池或 NIO)。注意处理粘包问题(上一篇的内容),以及超时、端口复用等工程细节。
本篇要点
- Socket 是操作系统提供的网络编程接口,应用程序通过 Socket API 使用 TCP/UDP 进行通信
- 服务端核心流程:ServerSocket 监听 → accept() 阻塞等待连接 → 获取流读写数据 → 关闭
- 多线程服务端:主线程循环 accept(),每个连接分配一个独立线程处理(生产环境用线程池或 NIO)
- TCP_NODELAY 关闭 Nagle 算法,适合延迟敏感场景;SO_REUSEADDR 解决 TIME_WAIT 端口占用
- 半关闭:
socket.shutdownOutput()关闭输出(发 FIN)但输入流仍可读 - BIO 每连接一线程(阻塞);NIO 一线程管多连接(Selector 轮询);AIO 操作系统完成读写后回调