三次握手,四次挥手:你的 connect() 和 close() 在 TCP 栈里经历了什么?
你用 Java 的
Socket连接服务器,一行new Socket(host, port)就搞定了。
但在这行代码背后,操作系统内核为你悄悄完成了三次握手:SYN、SYN+ACK、ACK。
当你关闭连接时,又是四次挥手:FIN、ACK、FIN、ACK。
每一个状态转变,都对应着你代码里的connect()、accept()、close()。
而TIME_WAIT状态,更是让无数后端工程师抓狂的“端口被占用”元凶。
大家好,我是 Evan,一个用 netstat -an | grep TIME_WAIT 排查过线上服务端口耗尽的 Java+AI 学生。
今天,我从 TCP 的三次握手和四次挥手讲起,带你看看 Java 的 Socket、ServerSocket 和 close() 背后到底发生了什么。读完这篇,你不仅能画出 TCP 状态机,还能理解为什么高并发下会出现 Address already in use。

📌 写在前面
大二学计网,我死记硬背了三次握手和四次挥手的图,但总觉得那是路由器和交换机的事,跟我的 Java 代码无关。
直到我在知识汇教育平台做一个长连接服务,压测时频繁报 BindException: Address already in use,查了半天发现是 TIME_WAIT 堆积。
那一刻我才明白:每一次你调用 close(),内核都帮你维护了一个定时器。
这篇博客,我就用 Java 开发者的语言,把 TCP 握手挥手的状态机讲透。
一、三次握手:建立连接的“礼尚往来”
1.1 为什么需要三次?两次不行吗?
三次握手的核心目的:让双方确认自己的发送和接收能力正常,并且协商初始序列号(ISN)。
两次握手无法防止已过期的连接请求突然到达造成的混乱。
1.2 握手过程与状态转移


Java 视角:
-
new Socket("localhost", 8080)会阻塞直到三次握手完成(或超时)。 -
ServerSocket.accept()在三次握手完成后才会返回一个新的Socket对象。
1.3 connect() 超时与 accept() 阻塞
Socket socket = new Socket();
socket.connect(new InetSocketAddress("host", 8080), 2000); // 超时 2 秒
如果在规定时间内没完成握手,抛出 SocketTimeoutException。
而 ServerSocket.accept() 会一直阻塞,直到有客户端完成三次握手。
二、四次挥手:优雅地告别
2.1 为什么需要四次?因为 TCP 是全双工的
每一方都需要单独关闭自己的发送通道。
主动关闭方发送 FIN,被动方回复 ACK,然后被动方也发送 FIN,主动方回复 ACK。

2.2 close() 与 TIME_WAIT
主动关闭方(谁先调用 close())会进入 TIME_WAIT 状态,持续 2 倍 MSL(Maximum Segment Lifetime,通常 30 秒到 2 分钟)。
为什么要有 TIME_WAIT?
-
确保最后一个
ACK能被对方收到(如果对方没收到会重发FIN)。 -
让网络上残留的旧数据包消失,避免影响新连接。
问题:高并发短连接场景(如压测),大量 TIME_WAIT 会占用本地端口,导致 Address already in use。
Java 中的对应:
socket.close(); // 触发 TCP 四次挥手的主动关闭
2.3 SO_REUSEADDR 选项
可以通过设置 socket 选项允许重用 TIME_WAIT 状态的端口:
ServerSocket server = new ServerSocket();
server.setReuseAddress(true);
server.bind(new InetSocketAddress(port));
但要注意风险:可能收到老连接的残留数据。
三、常见开发场景与排查命令
3.1 查看 TCP 连接状态
netstat -an | grep -E "ESTABLISHED|TIME_WAIT|CLOSE_WAIT"
-
ESTABLISHED:正常通信的连接。 -
TIME_WAIT:主动关闭方等待 2MSL,过多会影响端口。 -
CLOSE_WAIT:被动关闭方收到 FIN 后未调用close()(代码 bug 常见:服务端忘记关闭连接)。
CLOSE_WAIT 泄漏:如果你的 Java 服务不断出现 CLOSE_WAIT,说明你没有在 finally 块里关闭 socket 或 channel。
3.2 调整系统参数缓解 TIME_WAIT
Linux 下可以修改:
# 开启快速回收(不推荐新内核)
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 0 # 已废弃
# 减少 TIME_WAIT 持续时间(不建议低于 30 秒)
net.ipv4.tcp_fin_timeout = 30
更推荐:使用连接池(如 HikariCP、HttpClient 连接池)复用连接,避免频繁创建和关闭。
3.3 ServerSocket.accept() 与多线程
每个新连接 accept() 后通常交给线程池处理,别忘了在 finally 中关闭 Socket:
Socket client = server.accept();
executor.submit(() -> {
try {
// 处理请求
} finally {
client.close(); // 触发四次挥手
}
});
四、常见问题与陷阱
4.1 服务端大量 CLOSE_WAIT
原因:服务端收到客户端 FIN 后,没有调用 close()。
典型错误:线程池处理请求时,只关闭了输入输出流,但没关闭 socket 本身。
解决:使用 try-with-resources 或确保 finally 中关闭 socket。
4.2 客户端大量 TIME_WAIT
原因:短连接场景,客户端主动关闭。
解决:改用长连接(HTTP Keep-Alive)或连接池。
4.3 connect 超时 vs read 超时
-
connect超时:三次握手未完成。 -
read超时:连接已建立但对方迟迟不发送数据(setSoTimeout)。
4.4 Socket 的 close() 与 shutdownOutput()
-
close():完全关闭,发送 FIN。 -
shutdownOutput():半关闭,只关闭输出方向,仍可读。常用于通知对端“我不再写数据”。
📝 总结
核心结论:
-
TCP 握手/挥手是可靠传输的基础,每个状态都对应内核行为。
-
Java 开发者要特别注意
CLOSE_WAIT(忘记关闭)和TIME_WAIT(短连接过多)。 -
用连接池、复用连接、合理设置
SO_REUSEADDR可以缓解端口耗尽问题。
🤔 思考题:
你写了一个 HTTP 服务器,用 ServerSocket.accept() 接收请求,每次处理完请求后调用 socket.close()。
压测 10000 个短连接后,发现客户端报 BindException: Address already in use。
你用 netstat -an | grep TIME_WAIT 看到大量 TIME_WAIT 连接,都是客户端端口。
问题:为什么客户端会大量 TIME_WAIT?如何在不修改内核参数的情况下减少这种现象?
欢迎在评论区留下你的方案 —— 下一篇我会聊聊 “UDP 的无连接特性:DNS 查询为什么喜欢用 UDP?”
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)