计算机网络篇17:传输层——TCP 超时重传、三次握手与四次挥手,TIME_WAIT 为什么要等 2MSL


📚 本文收录于「流浪」的系列专栏

🐧Linux系统⚙️C++
📊数据结构与算法🐍Python
🔗LangChain & LangGraph🗄️MySQL 数据库
🌿Git 工具🌐计算机网络
🤖LLM💯大厂面试、八股
📚学习筑基专栏

🏠 博客主页:流浪 | 📝 原创首发于 CSDN


前言:TCP 号称可靠传输,但可靠不是喊出来的:数据会丢,ACK 也会丢,连接要建、也要断。本篇从「收不到 ACK 怎么判定丢包」切进去,讲清超时重传与序号去重,再把三次握手的必要性、四次挥手的共识,以及TIME_WAIT 为什么必须等 2MSL串成一条线。


一、超时重传:收不到 ACK 不等于丢包

1.1 丢包分两种情况

1. 数据报文丢了

  1. 主机 A 把数据发给主机 B,报文在途中丢了,B 根本没见过这封报文,自然不会回 ACK。
  2. A 等不到确认,过一段时间只能重发。
  3. 这种情况下丢的是数据本身,B 完全不知情。
2. 应答报文丢了

  1. B 收到了数据,回的 ACK 在途中丢了。
  2. 从 A 的角度看,和上一种一模一样:等不到 ACK。
  3. 区别在于 B 已经收下数据,A 再重发时 B 会收到一份重复的报文。

两种丢失在发送方眼里长得一样,都是「等不到 ACK」——发送方没法只凭「没收到」区分是哪一种。

1.2 没收到 ACK 只说明「可能丢」

1. ACK 没来有三种可能
  1. 数据丢了,B 没收到,自然不会回。
  2. 数据到了,B 回了 ACK,但 ACK 在路上丢了。
  3. 数据到了,ACK 也没丢,只是还在路上,没走到 A。
2. 判定丢包要两个条件
  1. 单看「没收到 ACK」判不了丢包,它只能说明「可能丢」。
  2. 所以 TCP 加了第二个条件:等,等到超时。
  3. 两个条件同时成立——收不到应答、且超时——才判定丢包,触发重传。

只凭「没收到」就重传,会把还在路上的正常确认当成丢包,凭空制造重复报文;加上超时,才把「可能」收敛成「确定」。

1.3 超时时间怎么定

1. 理想的最小时间
  1. 理想情况是找一个最小的时间,保证「确认应答一定能在这个时间内返回」。
  2. 但这个时间的长短随网络环境不同而有差异:带宽、距离、拥塞程度都会影响一个报文来回要多久。
2. 太长和太短的代价
  1. 超时时间设得太长:丢包之后要干等很久才重传,整体重传效率被拉低。
  2. 超时时间设得太短:ACK 还在路上就被判成丢包,频繁发送重复的包,白占带宽。
  3. 两头都不讨好,所以这个值不能拍脑袋定死。
3. 动态计算与指数退避
  1. TCP 会动态计算这个最大超时时间,让它在不同网络环境下都能保持比较高的性能。
  2. Linux 中(BSD Unix 和 Windows 也是如此),超时以 500ms 为一个单位进行控制,每次判定超时重发的超时时间都是 500ms 的整数倍。
  3. 如果重发一次之后仍然得不到应答,等待 2*500ms 后再重传。
  4. 如果仍然得不到应答,等待 4*500ms 重传,依次类推,以指数形式递增。
  5. 累计到一定的重传次数,TCP 认为网络或者对端主机出现异常,强制关闭连接。

超时时间不是一个固定值,而是一组按 500ms 起步、逐次翻倍的等待窗口;翻到一定次数还不通,TCP 就认定这条路走不通了。

1.4 序号的三个作用

1. 确认应答
  1. TCP 给发送的每一个字节都编了号,发送方发到哪、接收方收到哪,都靠序号说清楚。
  2. 接收方回 ACK 时带上确认号:前 N 个字节我收齐了,下一个要 N+1。
2. 按序到达
  1. 报文可能乱序到达,接收方按序号重新排列。
  2. 应用程序读到的永远是排好序的字节流,不用操心网络里的乱序。
3. 去重
  1. 重传会让同一段数据在网络里出现两份。
  2. 接收方靠序号判断:这段数据我收过了,重复的那份直接丢掉。

序号不只是「给数据编个号」,它把确认应答、按序到达、去重三件事一起办了——重传敢发,就是因为接收方有办法认出重复。

二、连接管理:每条连接先描述,再组织

2.1 一条连接要记的东西不少

1. 连接的身份
  1. 一条 TCP 连接由四元组唯一确定:源 IP、源端口、目标 IP、目标端口。
  2. 同一个服务端要同时服务很多客户端,靠四元组才能分清哪个报文属于哪条连接。
2. 连接的状态
  1. 每条连接还带着序号、确认号、窗口大小,以及自己当前处在哪个阶段——是在等对方确认,还是已经能收发数据,抑或已经关闭、在等旧报文消散。
  2. 阶段决定这条连接下一步能干什么,内核必须随时能查到它。

连接不是一句「连上了」就完了,它是一份要持续维护的状态记录,内核得先把它描述清楚。

2.2 先描述,再组织

1. 先描述:一条连接一个结构体
  1. 内核用一个结构体把一条连接的所有属性装进去:四元组、序号、窗口、状态、收发缓冲区。
  2. 这个结构体就是「描述」——把一条连接的静态属性和动态状态都落到字段上。
2. 再组织:一堆结构体怎么摆
  1. 服务端同时有成百上千条连接,就对应成百上千个结构体。
  2. 收到一个报文,内核要快速找到它属于哪条连接,不能让所有连接排成一队挨个比对。
  3. 把这些结构体按哈希表之类的结构组织起来,用四元组做键,才能做到快速定位。

「先描述,再组织」是内核管理连接的基本手法:描述解决「一条连接有哪些信息」,组织解决「大量连接怎么快速找到」。

2.3 connect 触发握手,accept 只取结果

1. 握手由内核完成
  1. 应用层调用 connect,内核负责发出 SYN、接收 SYN+ACK、再回 ACK。
  2. 三次握手由 C 端的 OS 自动完成,应用层只看到 connect 阻塞或返回,感知不到中间三次报文。
2. accept 不参与握手
  1. accept 的作用是从内核的已完成连接队列里取出一条连接,返回一个新的 socket 文件描述符。
  2. 连接被 accept 之前,三次握手早就完成了;accept 拿到的是一条已经建好的连接。
  3. 《计算机网络篇7:传输层——TCP 网络编程常用接口与调用流程》 把 connect、listen、accept 这套接口的调用流程串过一遍,这里接着看它们背后触发了什么。

connect 负责「建」,accept 负责「取」;握手发生在 connect 这一侧的内核里,accept 只是把建好的结果交到应用层手里。

三、三次握手:用最小成本验证全双工

3.1 为什么要三次握手

1. 验证全双工
  1. TCP 是全双工的:双方都能发、都能收。
  2. 握手要确认的不是「我能发」,而是「我发出去对方能收、对方发出来我能收」——四个方向都要验证。
2. 最小成本 100% 确认双方意愿
  1. 次数少了,有一方的接收能力没被确认,连接就是半信半疑的。
  2. 次数多了,每多一次都是在重复已经确认过的信息,纯属浪费。
  3. 三次是「刚好够」的次数:用最少的报文,把四个方向全部确认一遍。

验证全双工的本质,是检验双方所处的网络环境是否通畅、能否支持双向通信;三次握手用最少的报文,同时把四个方向和双方的通信意愿确认一遍。

3.2 逐次握手点亮了哪些能力

1. 一次握手:只验证了 C 到 S
  1. C 发 SYN,S 收到后确认「C 能发、S 能收」。
  2. C 只知道自己发出去了,不知道 S 有没有收到,更不知道 S 能不能发。
  3. 此时只点亮了 C→S 这一个方向。
2. 二次握手:C 这边齐了,S 还差一个
  1. S 回 SYN+ACK,C 收到后确认「S 能发、C 能收」。
  2. 加上第一次握手,C 已经能确认四件事:C 能发、S 能收、S 能发、C 能收——全双工在 C 的视角里齐了。
  3. 但 S 不知道 C 有没有收到自己的 SYN+ACK,所以 S 还确认不了「C 能收」。
3. 三次握手:S 也齐了,全双工成立
  1. C 回 ACK,S 收到后确认「C 能收」。
  2. 至此双方都拿到了四个方向的确认,全双工验证完成。
  3. 两次不够的根子就在这里:S 的「C 能收」始终没被确认,S 发数据可能全部打水漂。

三次握手拆开看,就是 C 先证明自己能发,S 再证明自己能发,最后 C 用 ACK 让 S 确认自己也能收——四个方向一个不落。

3.3 三次握手本质是四次握手合并

1. 原始的四步
  1. C 发 SYN(同步请求)→ S。
  2. S 发 ACK(确认收到 SYN)→ C。
  3. S 发 SYN(同步自己的请求)→ C。
  4. C 发 ACK(确认收到 S 的 SYN)→ S。
2. 第二、三步合并成 SYN+ACK
  1. S 的 ACK 和 SYN 可以放进同一个报文,因为这两件事之间没有要等的东西。
  2. ACK 搭 SYN 的便车一起发出去,这就是捎带应答。
  3. 原本的四步并成三步,握手次数从四次降到三次。

三次握手是四次握手「合并同类项」的结果:S 的确认和请求一起发,省下一封报文。

四、四次挥手与 TIME_WAIT:断开连接的共识

4.1 四次挥手四步

1. 四步流程
  1. C 发 FIN(请求关闭)→ S。
  2. S 回 ACK(确认收到 FIN)→ C。
  3. S 发 FIN(请求关闭)→ C。
  4. C 回 ACK(确认收到 S 的 FIN)→ S。
2. 断开连接的本质
  1. 每一步都是「请求 + 确认」的配对:FIN 是请求,ACK 是确认。
  2. 四步走完,两边都确认了「我不发了」和「我知道你不发了」。

断开连接的本质,是双方建立「断开连接」的共识:不是一方说关就关,而是两边都同意、都确认。

4.2 为什么通常四次而不是三次

1. 二、三步本可以合并
  1. 如果双方都没有数据要发,S 的 ACK 和 FIN 可以合成一个报文,挥手就变成三次。
  2. 但这需要一个前提:S 手里没有要发的数据了。
2. 常态是 S 还有数据没发完
  1. C 把消息发完、请求断开连接时,S 端可能还有数据没发完。
  2. S 不能立刻发 FIN,只能先回 ACK 稳住 C,等自己的数据发完再发 FIN。
  3. ACK 和 FIN 被拆成两个报文,所以通常走四次。

四次是常态,三次是双方都闲着时的合并特例——差别只在 S 还有没有尾数据要发。

4.3 TIME_WAIT 与 2MSL

1. 主动关闭方要等
  1. 先发 FIN 的一方是主动关闭方,它在发完最后一个 ACK 后进入 TIME_WAIT 状态。
  2. 即便四次挥手已经完成,它还要在 TIME_WAIT 里等 2MSL,才真正关闭连接。
2. 为什么等:最后那个 ACK 可能丢
  1. 主动关闭方发出的最后一个 ACK 有可能丢失,对端没等到确认,会重发 FIN。
  2. 主动关闭方还在 TIME_WAIT 里,就能再回一个 ACK,把对端从「等确认」里捞出来。
  3. 如果它立刻关闭,重发的 FIN 就没人理,对端会一直重发到超时。
3. 为什么等:旧报文不能污染新连接
  1. 新旧连接的四元组可能相同,旧连接滞留在网络里的报文,可能被新连接误收。
  2. 等 2MSL 让旧报文全部消散,新连接才干净。
4. 为什么是两倍 MSL
  1. MSL 是报文在网络里的最大生存时间。
  2. 一来一回:主动关闭方最后发的 ACK 最多活 1 MSL;对端重发的 FIN 也最多活 1 MSL。
  3. 所以等 2 MSL,两个方向上的旧报文都能死干净。

TIME_WAIT 等的不是「礼貌」,是两件事:让自己的最后一个 ACK 有机会重发,让旧连接的报文有时间死透。

4.4 TIME_WAIT 与端口复用

1. 端口为什么不能立即绑

  1. TIME_WAIT 期间,这条连接的四元组还被内核占着。
  2. 应用退出后立刻 bind 同一个端口,会失败,报地址已被占用(EADDRINUSE)。
2. 服务器崩溃重启怎么办
  1. 服务器意外崩溃,重启时想用原端口,被 TIME_WAIT 挡住,不能干等 2MSL。
  2. setsockopt 的 SO_REUSEADDR 可以让端口在 TIME_WAIT 期间也能绑定。
#include<sys/socket.h>intsetsockopt(intsockfd,intlevel,intoptname,constvoid*optval,socklen_t optlen);

3. 靠序列号区分新旧程序
  1. 立即复用端口的代价是:可能被旧连接的报文污染。
  2. 新连接的初始序列号是新的,旧报文的序列号对不上。
  3. 对不上的报文会被丢弃,不会污染新连接的数据。

端口复用是用「可能被污染」换「立即重启」,序列号就是兜底的那道筛子。


五、全篇总结

1. 超时重传靠两个条件判定
  1. 没收到 ACK 只说明可能丢,要「收不到应答 + 超时」才判定丢包。
  2. 超时时间以 500ms 为单位动态翻倍,累计到次数就强制关闭连接。
  3. 序号把确认应答、按序到达、去重三件事一起办了。
2. 连接由内核描述与组织
  1. 一条连接有四元组、序号、窗口、状态,内核先描述成结构体,再组织成表。
  2. connect 触发三次握手,由 C 端 OS 自动完成;accept 只取已建好的连接。
3. 三次握手验证全双工
  1. 两次不够:S 始终确认不了「C 能收」。
  2. 三次刚好:双方都拿到四个方向的确认。
  3. 它由四次握手合并而来,S 的 ACK 与 SYN 放进同一个报文。
4. 四次挥手与 TIME_WAIT
  1. 通常四次,因为 S 的数据没发完,ACK 与 FIN 拆成两个报文。
  2. 主动关闭方等 2MSL:保证最后的 ACK 能送达、旧报文能消散。
  3. TIME_WAIT 期间端口不能立即复用,SO_REUSEADDR 可以绕过,靠序列号防污染。

六、文末面试题

6.1 推导题

1. 为什么「收不到 ACK」不能直接判定丢包?两个条件缺一不可吗?

答(推导):收不到 ACK 至少有三种可能,数据丢了、数据到了但 ACK 丢了、ACK 还在路上只是延迟了,发送方分不清是哪一种,只能判定「可能丢」。所以判定丢包要两个条件同时成立:收不到应答,且等到超时。少一个都不行——只凭没收到 ACK 就重传,会把还在路上的正常确认当成丢包,凭空制造重复报文;只凭超时而不看有没有应答,又没有判定依据。

2. 三次握手为什么少一次不够、多一次浪费?

答(推导):两次握手后,C 已经能从自己的视角确认四个方向——C 能发、S 能收、S 能发、C 能收;但 S 只知道 C 能发、自己能收、自己能发,唯独不知道 C 有没有收到自己的 SYN+ACK,也就是「C 能收」没被确认,S 此时发数据可能全部打水漂。第三次握手的 ACK 让 S 确认「C 能收」,四个方向才算齐。第三次之后双方都齐了,再加一次没有任何新信息,纯属浪费。

3. TIME_WAIT 为什么等 2MSL,不等会怎样?

答(推导):主动关闭方发完最后一个 ACK 后,这个 ACK 有可能丢失,对端会重发 FIN。如果主动关闭方立刻关闭,重发的 FIN 就没人回应,对端会一直重发到超时。等 1 个 MSL 能覆盖「我的 ACK 在网络里死掉」;但如果对端重发了 FIN,这个 FIN 还要再活 1 个 MSL,所以一共要等 2 个 MSL。另一层原因是让旧连接滞留在网络里的报文全部消散,避免四元组相同的新连接被旧报文污染。

6.2 真题

1. 三次握手的过程是怎样的?为什么要三次而不是两次?

答(推导 · 已对照解析,转述):客户端发 SYN,服务端回 SYN+ACK,客户端再回 ACK,三次握手完成。两次不够的原因是服务端无法确认客户端收到了自己的 SYN+ACK,也就无法确认客户端有接收能力;客户端的 ACK 补齐了这个确认,同时避免历史连接请求造成错误建连。

【真题·转述自 牛客网面经帖】

2. 四次挥手的过程是怎样的?

答(推导 · 已对照解析,转述):主动关闭方发 FIN,被动关闭方回 ACK;等被动关闭方的数据发完,再发 FIN,主动关闭方回 ACK,连接关闭。主动关闭方在发完最后一个 ACK 后进入 TIME_WAIT,等待 2MSL。

【真题·转述自 牛客网面经帖】

3. 为什么 TIME_WAIT 状态要等待 2MSL?

答(推导 · 已对照解析,转述):一是保证最后一个 ACK 能到达对端,如果 ACK 丢失,对端重发的 FIN 还能被 TIME_WAIT 状态接住并再回 ACK;二是让旧连接的重复报文在网络中消散,防止污染四元组相同的新连接。2MSL 是一来一回的时长:ACK 最多活 1 MSL,重发的 FIN 也最多活 1 MSL。

【真题·转述自 牛客网面经帖】

4. TCP 第三次握手的 ACK 丢失会怎样?

答(推导 · 已对照解析,转述):客户端发出 ACK 后即认为连接建立,开始发数据;服务端没收到 ACK,认为握手没完成。服务端收到不属于任何已建立连接的数据报文,会回 RST 要求重新建连,客户端只能重新握手。

【真题·转述自 牛客网面经帖】

5. TCP 超时重传的时间是怎么确定的?

答(推导 · 已对照解析,转述):理想情况是找一个能保证 ACK 一定返回的最小时间,但网络环境不同,这个时间有差异;设太长影响重传效率,设太短会频繁发重复包。所以 TCP 动态计算超时时间,Linux 等系统以 500ms 为单位,每次重传按 2 倍指数递增,累计到一定次数就认为网络或对端异常,强制关闭连接。

【真题·转述自 牛客网面经帖】

6. 四次挥手为什么通常是四次,什么情况下可以变成三次?

答(推导 · 已对照解析,转述):通常四次是因为被动关闭方收到 FIN 时可能还有数据没发完,只能先回 ACK,等数据发完再发 FIN,ACK 与 FIN 拆成两个报文。如果双方都没有数据要发,被动关闭方的 ACK 和 FIN 可以合并成一个报文(捎带应答),四次挥手就变成三次。

【真题·转述自 牛客网面经帖】


💬结语:超时靠「没应答 + 超时」判定,三次握手把四个方向验齐,TIME_WAIT 的两倍 MSL 等的是一来一回——连接的建立与释放,本质上都是拿最少的报文换双方都认的共识。评论区聊聊你抓包时见过最长的 TIME_WAIT 挂了多久。