Skip to content
Go back

为什么四次挥手要等2MSL

一句话结论(30s)

主动关闭方发完最后一个 ACK 后还要等 2MSL(Linux 下 60 秒)才进入 CLOSED,原因有二:① 确保最后的 ACK 被对端收到——若 ACK 丢失,对端会重传 FIN,TIME_WAIT 期间还能再回 ACK;② 让旧连接的所有残留报文在网络中彻底死透——防止相同四元组的新连接收到”前世”迷路数据。这也是服务重启报 “Address already in use” 的根源:端口还处在 TIME_WAIT 没释放。

核心原理(2min)

底层深入(5-10min)

在日常开发中,你大概率遇见过”Address already in use”这个错误。服务重启时端口被占用,到底是什么占着不放?答案就藏在TCP四次挥手的最后一环——TIME_WAIT状态,以及那个经典的”2MSL等待”。

回顾四次挥手的完整状态转换

主动关闭方发FIN → 进入FIN_WAIT_1 → 收到ACK → 进入FIN_WAIT_2 → 收到被动方的FIN → 发送ACK → 进入TIME_WAIT → 等待2MSL后 → 进入CLOSED。

问题在于:为什么最后发了ACK还要等?ACK发完了不就收工了吗?

MSL到底是什么

MSL(Maximum Segment Lifetime)是指一个TCP报文段在网络中能够存活的最长时间。超出这个时间,报文就被认为已”死亡”,路由器会丢弃它。

MSL不是协议标准定义的固定值,而是操作系统的实现选择:

这就是为什么同一套TCP协议的服务器,在Linux和Windows上服务重启时端口释放时间完全不同。

MSL vs TTL:两个容易被混淆的概念

很多人会把MSL和IP头里的TTL(Time To Live)混为一谈,但它们是完全不同的东西:

两者的关系是:MSL应该大于TTL转换为时间的上限。因为如果MSL小于某个路由路径的总TTL时间,报文还没被IP层丢弃就被TCP层判定为过期,逻辑就不一致了。

思考:MSL 和 TTL 都跟「报文寿命」有关,怎么不混?记住层级不同——TTL 在 IP 层,是「跳数计数器」,每过一路由器减 1,防的是「路由环路里打转」;MSL 在 TCP 层,是「时间上限」,防的是「报文在网络里待太久」。一个是数跳数、一个是掐时间,MSL 要大于 TTL 换算出的最大时间,二者逻辑才自洽。

为什么是2MSL,不是1MSL

核心原因有两个,本质上是同一个问题的两面:

原因一:确保最后一个ACK能被对端收到。 主动关闭方发最后一个ACK后,如果这个ACK丢了,被动关闭方收不到ACK就会重传FIN。ACK在网络中最长存活1个MSL,被动方重传的FIN最长也存活1个MSL。2MSL = 最大等待时间,确保即使最坏情况(ACK丢了、FIN重传)也能收到并重新ACK。如果在TIME_WAIT期间收到了对端的重传FIN,计时器重置,重新开始等待2MSL。

💭 思考:最后一个 ACK 丢了,为什么只能靠 TIME_WAIT 兜底,而不是靠别的重传机制?——因为这是挥手“最后一脚”,ACK 一旦发出,主动方手里就再也没有“还要等什么”的任务了;而且连接已经半死,常规的数据重传定时器不会管它。只有让主动方在 TIME_WAIT 里多等 2MSL,才有机会收到对端因 ACK 丢失而重传的 FIN,再补一个 ACK。反过来说,如果不等就直接 CLOSED,重传的 FIN 到达时连接已不存在,内核只能回 RST,把本该“优雅收尾”的关闭变成“异常终止”。

原因二:防止旧连接的数据干扰新连接。 这是一个更深刻的原因。考虑这个场景:

  1. 连接A(源IP:Port, 目的IP:Port)正常关闭,进入TIME_WAIT。
  2. 如果不用等2MSL直接释放,接着立刻在同样的四元组上创建连接B。
  3. 此时网络中还有一个连接A的迷失报文(在网络设备中被缓存或绕路了很久),恰好在连接B建立后到达。
  4. 这个报文的序列号恰好落在连接B的接收窗口内。
  5. 连接B收到了”前世”的数据——数据错乱。

2MSL等待确保了网络中所有属于旧连接的报文都已经被丢弃,新连接不会被旧数据污染。1个MSL让”迷路”报文被丢弃,另一个MSL让对该报文的”响应”也被丢弃——两个方向都清干净。

思考:为什么「旧包污染新连接」这个原因更深刻?因为它揭示了 2MSL 是在给「可能迷路的报文」判死刑。设想连接 A 关闭后立刻在同四元组上建连接 B,此时网络里还飘着一个 A 的迷路包,序列号又恰好落在 B 的接收窗口内——B 就收到了「前世」的数据。等 2MSL 就是确保这些迷路包和「对它的响应」两个方向都清干净,新连接才能「清白」地开始。

实战中TIME_WAIT带来的问题与解决

高并发场景下,很多短连接同时关闭会产生大量TIME_WAIT状态的连接,耗尽端口资源。解决方案:

💭 思考:SO_LINGER 用 RST 跳过 TIME_WAIT,看起来省了 60 秒,为什么却说它危险?——因为 RST 是“暴力拆线”,不经过四次挥手的优雅确认:发 RST 的一方不保证对端已经收到并处理完所有数据,接收方也可能还有没读完的字节。跳过 TIME_WAIT 等于放弃了“确保最后 ACK 到达 + 让旧包消亡”这两道保险,省下的是等待时间,丢掉的是可靠性。所以它只适合“确认数据真的不想要了”的场景,生产环境要慎用。

总结

2MSL不是拍脑袋想出来的数字,它是”网络中最长迷路报文的来回时间”的上限估计。等够2MSL,就是给旧连接的所有残留报文判了死刑,让它彻底从网络中消失——这是TCP协议保证可靠性的最后一道防线。

💭 思考:为什么偏偏是 2 个 MSL,不是 1 个也不是 3 个?——因为最坏情况是“一来一回”:最后 ACK 最长活 1 个 MSL(去程),对端重传的 FIN 最长也活 1 个 MSL(回程),加起来正好 2 个 MSL,才能保证“哪怕 ACK 丢了、FIN 重传了,也都能在 TIME_WAIT 内碰到”。1 个 MSL 覆盖不了这个往返;3 个 MSL 则是多余——因为最长的一来一回也就 2 个 MSL,再多等纯属浪费端口。

章末提问

Q1:为什么四次挥手后主动关闭方要等 2MSL?

结论先行:为了确保最后的 ACK 被对端收到,并让旧连接的迷路报文彻底消亡。因为 ACK 丢了对端会重传 FIN,TIME_WAIT 期间还能再回 ACK(否则回 RST 导致异常终止);同时等 2MSL 让所有属于旧连接的延迟报文被网络丢弃,防止污染同四元组的新连接。

Q2:MSL 和 TTL 有什么区别?

结论先行:TTL 是 IP 层的跳数计数器,MSL 是 TCP 层的时间限制。因为 TTL 每过一路由器减 1、减到 0 丢弃,防的是路由环路;MSL 是报文在网络中存活的最长时间,防的是报文过期,且 MSL 应大于 TTL 对应的最大时间,两者层级和作用都不同。

Q3:TIME_WAIT 会带来什么问题?怎么解决?

结论先行:大量短连接会堆积 TIME_WAIT、耗尽端口,报 Address already in use;解决靠 SO_REUSEADDR 复用端口、长连接减少开关、必要时 tcp_tw_reuse。因为 TIME_WAIT 端口被占用、新连接分配不到端口;SO_REUSEADDR 允许重用 TIME_WAIT 端口,长连接从源头减少 TIME_WAIT 产生,SO_LINGER 发 RST 跳过的激进做法有丢数据风险不建议生产用。


Share this post on:

Previous Post
服务端大量TIME_WAIT:原因、危害与解决方案
Next Post
为什么四次挥手中间两次不能合并