2. TCP 四次挥手与 TIME_WAIT(热度 654 · 核心必考)
Q: 描述四次挥手过程。为什么是四次?TIME_WAIT 为什么要等 2MSL?
A:
完整过程(报文 + 状态机)
diagram
主动关闭方(A) 被动关闭方(B)
| ESTABLISHED | ESTABLISHED
|--- FIN=1, seq=u ------------->| CLOSE-WAIT
| FIN-WAIT-1 | (应用仍可发数据)
|<-- ACK=1, ack=u+1, seq=v -----|
| FIN-WAIT-2 |
| (B 继续发送剩余数据) |
|<-- FIN=1, seq=w --------------| LAST-ACK
|--- ACK=1, ack=w+1 ----------->|
| TIME-WAIT(等2MSL) | CLOSED(收到ACK后)
| → CLOSED |- A→B:FIN=1, seq=u:A 告诉 B「我没有数据要发了」(半关闭:仍可接收)。A 进入
FIN-WAIT-1。 - B→A:ACK=1, ack=u+1:B 确认。B 进入
CLOSE-WAIT。此时 B 应用层可能还有数据要发完。 - B→A:FIN=1, seq=w:B 数据发完,也发 FIN。B 进入
LAST-ACK。 - A→B:ACK=1, ack=w+1:A 确认,进入
TIME-WAIT,等待 2MSL 后进入CLOSED。B 收到 ACK 后CLOSED。
补充状态: 若 A 在 FIN-WAIT-1 收到 FIN(B 也立刻关),可同时确认,进入 CLOSING,再收 ACK 后 TIME-WAIT;或 B 的 ACK 与 FIN 一起到,A 直接从 FIN-WAIT-1 进 TIME-WAIT。
设计动机:为什么是四次?
- TCP 全双工,每个方向要单独关闭。 FIN 只表示「这个方向我不再发送」。
- 被动方收到 FIN 后可能还有数据要发,所以 ACK(确认你的 FIN)和 FIN(我也关了)不保证能合并——这正是不像握手能省一次的原因。
- 半关闭(half-close)有实际价值: 如客户端发完请求后 shutdown write,服务器仍能把剩余响应发完再关。
TIME_WAIT 等 2MSL 的原因
MSL(Maximum Segment Lifetime)是报文最大生存时间。
- 保证最后一个 ACK 能到达: 若最后的 ACK 丢失,被动方会重传 FIN;主动方仍在 TIME-WAIT 才能再发 ACK。若主动方立刻消失,被动方会卡在 LAST-ACK。
- 让本次连接的旧报文在网络中自然消亡: 等待超过「报文最长寿命」后,相同四元组(源IP、源端口、目的IP、目的端口)的新连接不会误收旧报文。
Linux 实际: Linux 的 TIME-WAIT 时长是内核常量 TCP_TIMEWAIT_LEN = 60 秒,写死在 include/net/tcp.h 里,不可通过 sysctl 调整,也与 tcp_fin_timeout 无关(tcp_fin_timeout 管的是没有文件描述符引用的孤儿连接在 FIN-WAIT-2 的停留时间,默认 60s——两者常被混为一谈)。RFC 793 定义的 MSL = 2 分钟,按 2MSL 算应是 4 分钟;教材常写 2MSL(谢希仁第8版取 MSL=2 min ⇒ 4 min),Linux 用 60s 是工程折中(内核 tcp.h 里它就是个写死的常数,注释并未按 2×MSL 推导;RFC 793 的 MSL=2 min 若要 2MSL 应是 4 min。别临时编一个「报文最大存活 30s」去调和这两个数——那没有出处)。答题口径:规范是 2MSL,Linux 实现固定 60s。
加分点
- 高并发短连接服务器大量 TIME_WAIT:优化
tcp_tw_reuse(安全前提下复用)、tcp_max_tw_buckets、连接池/长连接。TIME_WAIT 是正常现象;CLOSE_WAIT 堆积才是 bug(见第 18 题)。 - 主动关闭方才会 TIME-WAIT:「谁 TIME_WAIT 谁主动关」——排查时看 TIME_WAIT 出现在服务端还是客户端。
tcp_tw_recycle因在 NAT 环境下时间戳不一致会误杀新连接,Linux 4.12 起已彻底删除(不是"不推荐",而是新版本根本没有这个开关);tcp_tw_reuse仍在,取值 0/1/2(0=关闭;1=全局启用;2=只对回环流量启用,内核ip-sysctl.rst原文为 "1 - global enable / 2 - enable for loopback traffic only")。默认值可核:Linux 4.17 及以前是 0/1 两值、默认 0;4.18 起默认 2(本机 7.0.0 内核sysctl net.ipv4.tcp_tw_reuse实测为 2)。可见默认值只允许复用回环连接的 TIME_WAIT 套接字,要让出站公网连接也复用需显式设成 1;另外复用还要求对端启用了时间戳、且该套接字进入 TIME_WAIT 至今超过tcp_tw_reuse_delay(默认 1000 ms;内核比的是tw->tw_entry_stamp,不是「上次收到对端时间戳」的tw_ts_recent_stamp)。
常见追问
追问 1:为什么握手 3 次、挥手 4 次?
- 答题要点: 握手时服务器 SYN+ACK 可合并;挥手时被动方的 ACK 与 FIN 不保证能合并——收到 FIN 只代表「对方不发了」,本方可能还有数据要发,分开走才能保住全双工与半关闭;但这是「不保证」不是「禁止」:本方无待发数据时两者可以同段到达,线上只剩 3 个报文(见上面状态机那条)。
追问 2:大量 TIME_WAIT 怎么处理?一定是问题吗?
- 答题要点: 不一定是问题,是主动关闭方短连接多的正常结果。问题在于耗尽本地端口/Ephemeral port 或文件描述符相关资源。处理:① 应用层连接池/HTTP keep-alive;②
net.ipv4.tcp_tw_reuse=1(允许对出站连接复用 TIME_WAIT);③ 调大端口范围ip_local_port_range;④ 别再去调tcp_tw_recycle——4.12 起已删除(见上),新内核根本没有这个开关。先用ss -tan state time-wait确认分布。
追问 3:如果服务器出现大量 CLOSE_WAIT,和 TIME_WAIT 有什么本质区别?
- 答题要点: TIME_WAIT 多 = 主动关闭方短连接多(可优化,不一定有 bug)。CLOSE_WAIT 多 = 本端收到了对端 FIN,但应用层迟迟没调用 close()——典型代码 bug(连接泄漏、异常未捕获、线程阻塞、连接池未归还)。见第 18 题。
易错点
| 易错说法 | 纠正 |
|---|---|
| 「TIME_WAIT 出现在被动关闭方」 | 出现在主动关闭方;被动方是 CLOSE_WAIT → LAST_ACK |
| 「2MSL 是为了让数据传完」 | 数据在 FIN/ACK 阶段已谈完发送方向;2MSL 为 ACK 可达性 + 旧报文消亡 |
| 「TIME_WAIT 很多说明网络故障」 | 多数情况是短连接负载高,属正常;故障信号更多是 CLOSE_WAIT/SYN_RECV 异常 |
| 「四次挥手是因为 TCP 不可靠」 | 四次源于全双工 + 半关闭语义,与可靠重传机制不是同一层原因 |
| 「服务器 TIME_WAIT 一定该关掉」 | 不能用内核 hack 一味清 TIME_WAIT;优先改连接模型 |
与选择题对照
- 主库 150: 第 60 题(握手/挥手次数)、第 61 题(第二步 SYN/ACK 的值)、第 62 题(主动关闭方最后一个报文=ACK)、第 63 题(TIME_WAIT 时长)、第 138 题(大量 CLOSE_WAIT 含义)。
- 补题 16: 补-11(TCP 标志位,FIN/ACK 等);与补-12 形成「建连/释放」对照。
- 口述联动: 选择题侧重「次数/标志位/时长」,面试侧重「为什么 4 次、2MSL 两个原因、TIME_WAIT vs CLOSE_WAIT」。
工程 / 抓包背景
- ss 命令:
ss -tan state time-wait | wc -lss -tan state close-waitss -tan sport = :80 or dport = :80
- Wireshark: 过滤
tcp.flags.fin==1;观察 FIN/ACK 时序;tcp.analysis.flags可看重传。 - sysctl:
net.ipv4.tcp_fin_timeout、net.ipv4.tcp_tw_reuse、net.ipv4.tcp_max_tw_buckets、net.ipv4.ip_local_port_range。 - Nginx:
keepalive_timeout、keepalive_requests;上游proxy_http_version 1.1; proxy_set_header Connection "";以复用上游连接,减少服务端主动关闭。