为什么是四次挥手而不是三次?
连接建立需要三次握手,断开连接为什么要四次?
这个问题看起来简单,但背后藏着 TCP 全双工通信的核心设计。
先说结论
四次挥手是因为 TCP 是全双工协议,两个方向需要独立关闭。
每个方向都是一次「我发完了 + 你确认」的过程,所以是四次。
从打电话说起
想象你和朋友打电话:
- 你说:好了,我的话说完了,你说吧。(第一次挥手)
- 朋友说:好的,我听到了。(第二次挥手)
- 朋友说:我也说完了,再见。(第三次挥手)
- 你说:好的,再见。(第四次挥手)
这才是一个完整的通话结束。
注意:第二步和第三步不能合并。因为你说完后,朋友可能还有话要说,要等朋友也说完,才能确认通话真正结束。
TCP 全双工的本质
TCP 是全双工(Full Duplex)协议:
- 数据可以同时双向传输
- 客户端可以发送数据给服务端,同时接收服务端的数据
- 每个方向都有独立的发送缓冲区和接收缓冲区
当你关闭连接时:
- 你关闭的是你的发送通道
- 但对方可能还有数据要发送给你
- 你需要接收完对方的数据后,才能关闭你的接收通道
┌─────────────────────────────────────────────────────────────┐
│ TCP 全双工通道 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 客户端 ────────────────────────────────> 服务端 │
│ │ │ │
│ │ 方向一:我 → 你 │ │
│ │ │ │
│ │ <────────────────────────────────────── │ │
│ │ │ │
│ │ 方向二:你 → 我 │ │
│ │
│ 关闭方向一:FIN + ACK(两次) │
│ 关闭方向二:FIN + ACK(两次) │
│ 总共:四次挥手 │
│ │
└─────────────────────────────────────────────────────────────┘四次挥手详细流程
方向一:客户端关闭发送通道
客户端 服务端
│ │
│ ────── FIN (seq=u) ────────────────> │ 第一次挥手
│ │
│ 客户端 → 服务端:我的数据发完了 │
│ │
│ <────── ACK (ack=u+1) ───────────────── │ 第二次挥手
│ │
│ 服务端 → 客户端:收到 │
│ │
│ 此时: │
│ - 客户端不能发送数据了 │
│ - 但可以接收服务端的数据 │
│ - 服务端不能发送数据给客户端了? │
│ 不!服务端还能发送数据给客户端 │方向二:服务端关闭发送通道
客户端 服务端
│ │
│ (服务端可能还有数据要发) │
│ <────── 数据 (seq=v) ────────────────── │
│ │
│ <────── FIN (seq=w) ────────────────── │ 第三次挥手
│ │
│ 服务端 → 客户端:我也发完了 │
│ │
│ ────── ACK (ack=w+1) ────────────────> │ 第四次挥手
│ │
│ 客户端 → 服务端:收到,再见 │
│ │为什么不能合并成三次?
假设:服务端在收到 FIN 后立即发送 FIN+ACK
客户端 ─── FIN ───────────────────────────────> 服务端
客户端 → 服务端:我发完了
客户端 <── FIN+ACK ─────────────────────────── 服务端
服务端 → 客户端:收到 + 我也发完了
客户端 ─── ACK ───────────────────────────────> 服务端问题:服务端可能还没发完数据!
场景:
1. 客户端发送 FIN,说「我的话说完了」
2. 服务端还有数据要发(还没来得及整理)
3. 服务端收到 FIN 后立即回复「我也发完了」,并发送 FIN
4. 但客户端已经开始等待,准备关闭
5. 服务端实际还有数据要发,但已经发了 FIN
结果:服务端的数据丢失,连接强制关闭正确流程:先 ACK,等数据发完再 FIN
第一步:收到 FIN,立即回复 ACK(确认收到,但不立即关闭)
→ 客户端知道服务端收到了,但不知道服务端有没有话要说
第二步:服务端发送完数据后,再发送 FIN
→ 客户端知道服务端也说完了
第三步:客户端回复最后的 ACK
→ 双方都确认对方完成了TIME_WAIT 的存在意义
为什么客户端要等待 2MSL?
客户端 ─── ACK ───────────────────────────────> 服务端
(ACK 可能丢失)
客户端 <── FIN(重发) ─────────────────────── 服务端
(如果客户端已经关闭,就收不到这个 FIN 了)- 防止 ACK 丢失:如果第四次挥手的 ACK 丢失,服务端会重发 FIN,客户端需要保持 TIME_WAIT 状态来响应
- 防止旧数据干扰:让网络中可能存在的旧数据包有足够时间消失,避免干扰新连接
服务端为什么不等待?
服务端收到最后的 ACK 后立即进入 CLOSED 状态。
如果服务端没收到 ACK,会重发 FIN,此时客户端已经消失了,服务端的连接会保持 LAST_ACK 状态直到超时。
特殊情况:同时关闭
有时候两端会同时发送 FIN:
客户端 服务端
│ │
│ ────── FIN ──────────────────────────> │
│ <─────── FIN ───────────────────────── │
│ │
│ 发送 ACK ────────────────────────────> │
│ <─────── ACK ───────────────────────── │
│ │
│ CLOSE_WAIT ──> CLOSING ──> TIME_WAIT │同时关闭的状态变化更复杂,但挥手次数还是四次。
为什么是四次不是五次?
四次挥手刚好关闭两个方向,每方向一次 FIN + ACK。
如果再多一次,就意味着某个方向要关闭两次,这是没有意义的。
实际案例:连接关闭的问题
场景一:大量 TIME_WAIT
bash
$ netstat -an | grep TIME_WAIT | wc -l
15234高并发短连接服务常见问题。
解决方案:
- 服务端开启
SO_REUSEADDR - 调整
tcp_tw_reuse和tcp_fin_timeout - 客户端使用 HTTP keep-alive
- 服务端使用连接池
场景二:大量 CLOSE_WAIT
bash
$ netstat -an | grep CLOSE_WAIT | wc -l
8567应用层没有正确关闭连接。
排查方法:
java
// 检查代码中的连接关闭逻辑
try (Socket socket = serverSocket.accept()) {
// 处理请求
} // 应该使用 try-with-resources 确保关闭场景三:优雅关闭 vs 强制关闭
java
// 优雅关闭:先关闭写,再读完数据,再关闭读
socket.shutdownOutput(); // 发送 FIN,等待对方数据
// ... 读取对方数据 ...
socket.shutdownInput(); // 收到对方的 FIN
socket.close(); // 发送最后的 ACK
// 强制关闭:立即发送 RST
socket.setSoLinger(true, 0);
socket.close(); // RST面试追问方向
- TCP 为什么是全双工?和半双工有什么区别?
- 为什么四次挥手不能合并成三次?
- TIME_WAIT 状态的作用是什么?
- 如果第四次挥手的 ACK 丢失了会怎样?
- 什么是 CLOSE_WAIT 状态?大量 CLOSE_WAIT 是什么原因?
- 什么是优雅关闭?如何实现?
- SO_REUSEADDR 是如何工作的?
- 如何排查和解决服务器上大量 TIME_WAIT 的问题?
