TCP 四次挥手 · TIME_WAIT 完全图解

为什么是四次?TIME_WAIT 为什么要等 2MSL?CLOSE_WAIT 堆积怎么办?

四次挥手 TIME_WAIT / 2MSL CLOSE_WAIT 泄漏 面试高频

①为什么需要“挥手”:连接是全双工的

TCP 是全双工的:客户端→服务端、服务端→客户端是两条独立的通道。关闭连接时,每个方向都必须单独关闭——这就是“挥手要四次、握手只要三次”的根本原因。

建立连接时,服务端的 SYN(我要连你)和 ACK(我收到你的请求)可以合并在同一个报文里发出,所以三次搞定。但关闭时,一方说“我没数据要发了(FIN)”,另一方只能先回一个 ACK——因为它可能还有数据没发完,不能立刻也发 FIN。于是 ACK 和 FIN 得分开两次发,总共四次。

全双工:两条独立通道,各自单独关闭 客户端 Client 服务端 Server 通道A:C → S 通道B:S → C 建立:SYN+ACK 可合并 → 三次握手 关闭:A 关了 B 可能还有数据 → ACK 与 FIN 不能合并 所以:握手三次,挥手四次
补充:挥手其实也可能变成三次——如果被动关闭方在收到 FIN 时恰好也没有数据要发,它可以把 ACK 和自己的 FIN 合并发送(延迟确认场景),这时就退化成三次。但标准流程讲四次。

②四次挥手全流程(时序图)

假设客户端主动关闭(实际上服务端也可以主动 close)。每一步的报文与状态如下:

主动关闭方 Client 被动关闭方 Server ESTABLISHED ESTABLISHED ① FIN=1, seq=u FIN_WAIT_1 ② ACK=1, ack=u+1 CLOSE_WAIT FIN_WAIT_2 (继续发送剩余数据) ③ FIN=1, seq=v(数据发完了) LAST_ACK ④ ACK=1, ack=v+1 TIME_WAIT CLOSED 等 2MSL CLOSED 四次挥手:每方向一次 FIN + 一次 ACK
步骤方向报文发送方进入接收方进入
①C → SFIN=1, seq=uFIN_WAIT_1CLOSE_WAIT
②S → CACK=1, ack=u+1—FIN_WAIT_2(接收方)
③S → CFIN=1, seq=vLAST_ACK—
④C → SACK=1, ack=v+1TIME_WAITCLOSED
记忆口诀:“我关我的(FIN)→ 你确认(ACK)→ 你关你的(FIN)→ 我确认(ACK)”。只有主动关闭方才会进入 TIME_WAIT。

③状态机变迁:关键状态都什么意思

FIN_WAIT_1 / FIN_WAIT_2

主动方发出 FIN 后等对方 ACK;收到后进入 FIN_WAIT_2,此时半关闭:还能收对方数据,但不能再发。

CLOSE_WAIT

被动方收到 FIN 后进入,含义是“对方要关了,等我自己的应用层 close()”。停留过久=程序忘了关 socket。

LAST_ACK

被动方发完自己的 FIN,等最后一个 ACK。收到即 CLOSED。

TIME_WAIT

主动方发出最后 ACK 后进入,要等 2MSL 才真正释放。见下一节。

被动关闭方视角(服务端) ESTABLISHED CLOSE_WAIT LAST_ACK CLOSED 收FIN close() 收ACK CLOSE_WAIT 停留 = 应用层没调 close() 服务端出现大量 CLOSE_WAIT → 几乎一定是代码 bug(连接泄漏)

④TIME_WAIT:为什么要等 2MSL

MSL(Maximum Segment Lifetime)是报文在网络中的最大生存时间,Linux 默认 30 秒,所以 TIME_WAIT 默认持续 60 秒(2MSL)。等这么久有两个硬核理由:

理由一:可靠地终止连接

最后那个 ACK 可能丢失。如果丢了,被动方会重发 FIN。主动方若已直接关闭,就只能回 RST,对方会认为出错。留着 TIME_WAIT 才能重发 ACK,让连接正常收尾。

理由二:消散旧连接的迷途报文

若不等待就复用同一四元组(源IP:源端口, 目的IP:目的端口)建新连接,上一次连接迟到的旧报文可能被新连接误收。等 2MSL 保证旧报文在网络中自然消亡。

场景:最后的 ACK 丢了会怎样 Client Server ACK ✗ 丢失 超时重传 FIN TIME_WAIT 中 → 重发 ACK 收到 ACK → CLOSED 若没有 TIME_WAIT:Client 已关闭 → 回 RST → 服务端异常报错

TIME_WAIT 过多有什么危害

  • 占用端口资源:每个 TIME_WAIT 占一个本地端口,客户端高并发短连接时可能端口耗尽(尤其压测机、爬虫、Nginx 反代上游)。
  • 占用内存与连接表项:内核要维护该连接的控制块,量大了有开销。
注意:很多人误以为 TIME_WAIT 是“服务端的问题”。其实谁主动 close,谁产生 TIME_WAIT。Web 服务器若主动关连接(如 Nginx 对上游主动断连),服务端才会大量堆积。

怎么优化(面试答这几个)

手段做法说明
复用连接开启 Keep-Alive / 连接池首选:从源头减少主动关闭
让对方先关业务设计上让客户端主动断开把 TIME_WAIT 转移给客户端
端口复用net.ipv4.tcp_tw_reuse = 1允许把 TIME_WAIT 连接用于新出站连接(需时间戳支持)
放宽端口范围ip_local_port_range从默认约 2.8 万扩到 6 万
缩短等待调小 tcp_fin_timeout治标不治本,需谨慎
避坑:tcp_tw_recycle 已在 Linux 4.12+ 被移除——它会导致 NAT 环境下大量连接被拒。别再把这个当答案说,会被认为知识过时。

⑤CLOSE_WAIT 堆积:几乎一定是代码 bug

核心判断:服务端出现大量 CLOSE_WAIT,说明“对端已经把连接关了,但本端应用程序没有调用 close()”。这是连接泄漏,与内核参数无关,调参解决不了。

常见原因:

  • HTTP 客户端拿到响应后没有关闭连接 / 没有放回连接池(如忘记 resp.Body.Close())。
  • 异常处理分支里漏了 close,正常路径关了、异常路径没关。
  • 使用了连接池但连接未归还,池子泄漏。
  • 线程池 / 协程阻塞,导致 close 逻辑根本没执行到。
# 排查命令:统计各状态连接数
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

# 看哪些进程持有大量 CLOSE_WAIT
ss -tanp | grep CLOSE-WAIT

# 典型输出(危险信号)
# CLOSE_WAIT  8234
# ESTABLISHED  120
# TIME_WAIT    45

修复方向:在 finally / defer 里保证资源释放;使用带超时和最大空闲数的连接池;给 HTTP client 设置合理超时。

⑥面试高频追问

Q1:为什么挥手需要四次,握手只要三次?
因为 TCP 全双工,每个方向要单独关闭。握手时服务端的 SYN 和 ACK 可以合并发送;挥手时被动方收到 FIN 后可能还有数据没发完,只能先回 ACK,等数据发完再发自己的 FIN,所以 ACK 与 FIN 分开发,共四次。
Q2:TIME_WAIT 为什么是 2MSL 而不是 1MSL?
2MSL = 一个 MSL 保证自己发出的 ACK 能到达对端,另一个 MSL 保证若 ACK 丢失,对端重发的 FIN 能到达自己。正好覆盖“最坏情况的往返”,从而可靠终止连接并让旧报文消散。
Q3:TIME_WAIT 出现在哪一端?
主动关闭方。谁先调用 close()/发第一个 FIN,谁最后进入 TIME_WAIT。
Q4:服务器上有大量 TIME_WAIT,一定是出问题了吗?
不一定。若服务端是主动关闭方(如反向代理主动断上游),TIME_WAIT 多是正常现象,只要不导致端口耗尽就无需恐慌。真正危险的是CLOSE_WAIT 堆积——那是泄漏。
Q5:TIME_WAIT 和 CLOSE_WAIT 的区别,一句话概括?
TIME_WAIT 是主动关闭方的正常等待状态(内核行为);CLOSE_WAIT 是被动关闭方等应用层 close(应用层 bug)。
Q6:如果最后的 ACK 丢了会怎样?
被动方超时重传 FIN;主动方仍在 TIME_WAIT,会重发 ACK 并重置 2MSL 计时器,从而可靠关闭。
Q7:为什么不能让客户端直接发 RST 关闭?
RST 是异常终止:会丢弃接收缓冲区中未读的数据,且不给对端“优雅收尾”的机会,容易造成数据截断和错误日志。正常关闭应走 FIN。

✓一句话速记

全双工 → 两方向各关一次 → 四次挥手;主动方等 2MSL(TIME_WAIT)保证可靠终止 + 旧报文消散;被动方停在 CLOSE_WAIT 说明应用没 close,是 bug。