Skip to content

为什么是四次挥手而不是三次?

连接建立需要三次握手,断开连接为什么要四次?

这个问题看起来简单,但背后藏着 TCP 全双工通信的核心设计。

先说结论

四次挥手是因为 TCP 是全双工协议,两个方向需要独立关闭。

每个方向都是一次「我发完了 + 你确认」的过程,所以是四次。

从打电话说起

想象你和朋友打电话:

  1. 你说:好了,我的话说完了,你说吧。(第一次挥手)
  2. 朋友说:好的,我听到了。(第二次挥手)
  3. 朋友说:我也说完了,再见。(第三次挥手)
  4. 你说:好的,再见。(第四次挥手)

这才是一个完整的通话结束。

注意:第二步和第三步不能合并。因为你说完后,朋友可能还有话要说,要等朋友也说完,才能确认通话真正结束。

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_reusetcp_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 的问题?

基于 VitePress 构建