计算机网络篇17:传输层——TCP 超时重传、三次握手与四次挥手,TIME_WAIT 为什么要等 2MSL
📚 本文收录于「流浪」的系列专栏
| 🐧Linux系统 | ⚙️C++ |
| 📊数据结构与算法 | 🐍Python |
| 🔗LangChain & LangGraph | 🗄️MySQL 数据库 |
| 🌿Git 工具 | 🌐计算机网络 |
| 🤖LLM | 💯大厂面试、八股 |
| 📚学习筑基专栏 |
🏠 博客主页:流浪 | 📝 原创首发于 CSDN
前言:TCP 号称可靠传输,但可靠不是喊出来的:数据会丢,ACK 也会丢,连接要建、也要断。本篇从「收不到 ACK 怎么判定丢包」切进去,讲清超时重传与序号去重,再把三次握手的必要性、四次挥手的共识,以及TIME_WAIT 为什么必须等 2MSL串成一条线。
一、超时重传:收不到 ACK 不等于丢包
1.1 丢包分两种情况
1. 数据报文丢了
- 主机 A 把数据发给主机 B,报文在途中丢了,B 根本没见过这封报文,自然不会回 ACK。
- A 等不到确认,过一段时间只能重发。
- 这种情况下丢的是数据本身,B 完全不知情。
2. 应答报文丢了
- B 收到了数据,回的 ACK 在途中丢了。
- 从 A 的角度看,和上一种一模一样:等不到 ACK。
- 区别在于 B 已经收下数据,A 再重发时 B 会收到一份重复的报文。
两种丢失在发送方眼里长得一样,都是「等不到 ACK」——发送方没法只凭「没收到」区分是哪一种。
1.2 没收到 ACK 只说明「可能丢」
1. ACK 没来有三种可能
- 数据丢了,B 没收到,自然不会回。
- 数据到了,B 回了 ACK,但 ACK 在路上丢了。
- 数据到了,ACK 也没丢,只是还在路上,没走到 A。
2. 判定丢包要两个条件
- 单看「没收到 ACK」判不了丢包,它只能说明「可能丢」。
- 所以 TCP 加了第二个条件:等,等到超时。
- 两个条件同时成立——收不到应答、且超时——才判定丢包,触发重传。
只凭「没收到」就重传,会把还在路上的正常确认当成丢包,凭空制造重复报文;加上超时,才把「可能」收敛成「确定」。
1.3 超时时间怎么定
1. 理想的最小时间
- 理想情况是找一个最小的时间,保证「确认应答一定能在这个时间内返回」。
- 但这个时间的长短随网络环境不同而有差异:带宽、距离、拥塞程度都会影响一个报文来回要多久。
2. 太长和太短的代价
- 超时时间设得太长:丢包之后要干等很久才重传,整体重传效率被拉低。
- 超时时间设得太短:ACK 还在路上就被判成丢包,频繁发送重复的包,白占带宽。
- 两头都不讨好,所以这个值不能拍脑袋定死。
3. 动态计算与指数退避
- TCP 会动态计算这个最大超时时间,让它在不同网络环境下都能保持比较高的性能。
- Linux 中(BSD Unix 和 Windows 也是如此),超时以 500ms 为一个单位进行控制,每次判定超时重发的超时时间都是 500ms 的整数倍。
- 如果重发一次之后仍然得不到应答,等待 2*500ms 后再重传。
- 如果仍然得不到应答,等待 4*500ms 重传,依次类推,以指数形式递增。
- 累计到一定的重传次数,TCP 认为网络或者对端主机出现异常,强制关闭连接。
超时时间不是一个固定值,而是一组按 500ms 起步、逐次翻倍的等待窗口;翻到一定次数还不通,TCP 就认定这条路走不通了。
1.4 序号的三个作用
1. 确认应答
- TCP 给发送的每一个字节都编了号,发送方发到哪、接收方收到哪,都靠序号说清楚。
- 接收方回 ACK 时带上确认号:前 N 个字节我收齐了,下一个要 N+1。
2. 按序到达
- 报文可能乱序到达,接收方按序号重新排列。
- 应用程序读到的永远是排好序的字节流,不用操心网络里的乱序。
3. 去重
- 重传会让同一段数据在网络里出现两份。
- 接收方靠序号判断:这段数据我收过了,重复的那份直接丢掉。
序号不只是「给数据编个号」,它把确认应答、按序到达、去重三件事一起办了——重传敢发,就是因为接收方有办法认出重复。
二、连接管理:每条连接先描述,再组织
2.1 一条连接要记的东西不少
1. 连接的身份
- 一条 TCP 连接由四元组唯一确定:源 IP、源端口、目标 IP、目标端口。
- 同一个服务端要同时服务很多客户端,靠四元组才能分清哪个报文属于哪条连接。
2. 连接的状态
- 每条连接还带着序号、确认号、窗口大小,以及自己当前处在哪个阶段——是在等对方确认,还是已经能收发数据,抑或已经关闭、在等旧报文消散。
- 阶段决定这条连接下一步能干什么,内核必须随时能查到它。
连接不是一句「连上了」就完了,它是一份要持续维护的状态记录,内核得先把它描述清楚。
2.2 先描述,再组织
1. 先描述:一条连接一个结构体
- 内核用一个结构体把一条连接的所有属性装进去:四元组、序号、窗口、状态、收发缓冲区。
- 这个结构体就是「描述」——把一条连接的静态属性和动态状态都落到字段上。
2. 再组织:一堆结构体怎么摆
- 服务端同时有成百上千条连接,就对应成百上千个结构体。
- 收到一个报文,内核要快速找到它属于哪条连接,不能让所有连接排成一队挨个比对。
- 把这些结构体按哈希表之类的结构组织起来,用四元组做键,才能做到快速定位。
「先描述,再组织」是内核管理连接的基本手法:描述解决「一条连接有哪些信息」,组织解决「大量连接怎么快速找到」。
2.3 connect 触发握手,accept 只取结果
1. 握手由内核完成
- 应用层调用 connect,内核负责发出 SYN、接收 SYN+ACK、再回 ACK。
- 三次握手由 C 端的 OS 自动完成,应用层只看到 connect 阻塞或返回,感知不到中间三次报文。
2. accept 不参与握手
- accept 的作用是从内核的已完成连接队列里取出一条连接,返回一个新的 socket 文件描述符。
- 连接被 accept 之前,三次握手早就完成了;accept 拿到的是一条已经建好的连接。
- 《计算机网络篇7:传输层——TCP 网络编程常用接口与调用流程》 把 connect、listen、accept 这套接口的调用流程串过一遍,这里接着看它们背后触发了什么。
connect 负责「建」,accept 负责「取」;握手发生在 connect 这一侧的内核里,accept 只是把建好的结果交到应用层手里。
三、三次握手:用最小成本验证全双工
3.1 为什么要三次握手
1. 验证全双工
- TCP 是全双工的:双方都能发、都能收。
- 握手要确认的不是「我能发」,而是「我发出去对方能收、对方发出来我能收」——四个方向都要验证。
2. 最小成本 100% 确认双方意愿
- 次数少了,有一方的接收能力没被确认,连接就是半信半疑的。
- 次数多了,每多一次都是在重复已经确认过的信息,纯属浪费。
- 三次是「刚好够」的次数:用最少的报文,把四个方向全部确认一遍。
验证全双工的本质,是检验双方所处的网络环境是否通畅、能否支持双向通信;三次握手用最少的报文,同时把四个方向和双方的通信意愿确认一遍。
3.2 逐次握手点亮了哪些能力
1. 一次握手:只验证了 C 到 S
- C 发 SYN,S 收到后确认「C 能发、S 能收」。
- C 只知道自己发出去了,不知道 S 有没有收到,更不知道 S 能不能发。
- 此时只点亮了 C→S 这一个方向。
2. 二次握手:C 这边齐了,S 还差一个
- S 回 SYN+ACK,C 收到后确认「S 能发、C 能收」。
- 加上第一次握手,C 已经能确认四件事:C 能发、S 能收、S 能发、C 能收——全双工在 C 的视角里齐了。
- 但 S 不知道 C 有没有收到自己的 SYN+ACK,所以 S 还确认不了「C 能收」。
3. 三次握手:S 也齐了,全双工成立
- C 回 ACK,S 收到后确认「C 能收」。
- 至此双方都拿到了四个方向的确认,全双工验证完成。
- 两次不够的根子就在这里:S 的「C 能收」始终没被确认,S 发数据可能全部打水漂。
三次握手拆开看,就是 C 先证明自己能发,S 再证明自己能发,最后 C 用 ACK 让 S 确认自己也能收——四个方向一个不落。
3.3 三次握手本质是四次握手合并
1. 原始的四步
- C 发 SYN(同步请求)→ S。
- S 发 ACK(确认收到 SYN)→ C。
- S 发 SYN(同步自己的请求)→ C。
- C 发 ACK(确认收到 S 的 SYN)→ S。
2. 第二、三步合并成 SYN+ACK
- S 的 ACK 和 SYN 可以放进同一个报文,因为这两件事之间没有要等的东西。
- ACK 搭 SYN 的便车一起发出去,这就是捎带应答。
- 原本的四步并成三步,握手次数从四次降到三次。
三次握手是四次握手「合并同类项」的结果:S 的确认和请求一起发,省下一封报文。
四、四次挥手与 TIME_WAIT:断开连接的共识
4.1 四次挥手四步
1. 四步流程
- C 发 FIN(请求关闭)→ S。
- S 回 ACK(确认收到 FIN)→ C。
- S 发 FIN(请求关闭)→ C。
- C 回 ACK(确认收到 S 的 FIN)→ S。
2. 断开连接的本质
- 每一步都是「请求 + 确认」的配对:FIN 是请求,ACK 是确认。
- 四步走完,两边都确认了「我不发了」和「我知道你不发了」。
断开连接的本质,是双方建立「断开连接」的共识:不是一方说关就关,而是两边都同意、都确认。
4.2 为什么通常四次而不是三次
1. 二、三步本可以合并
- 如果双方都没有数据要发,S 的 ACK 和 FIN 可以合成一个报文,挥手就变成三次。
- 但这需要一个前提:S 手里没有要发的数据了。
2. 常态是 S 还有数据没发完
- C 把消息发完、请求断开连接时,S 端可能还有数据没发完。
- S 不能立刻发 FIN,只能先回 ACK 稳住 C,等自己的数据发完再发 FIN。
- ACK 和 FIN 被拆成两个报文,所以通常走四次。
四次是常态,三次是双方都闲着时的合并特例——差别只在 S 还有没有尾数据要发。
4.3 TIME_WAIT 与 2MSL
1. 主动关闭方要等
- 先发 FIN 的一方是主动关闭方,它在发完最后一个 ACK 后进入 TIME_WAIT 状态。
- 即便四次挥手已经完成,它还要在 TIME_WAIT 里等 2MSL,才真正关闭连接。
2. 为什么等:最后那个 ACK 可能丢
- 主动关闭方发出的最后一个 ACK 有可能丢失,对端没等到确认,会重发 FIN。
- 主动关闭方还在 TIME_WAIT 里,就能再回一个 ACK,把对端从「等确认」里捞出来。
- 如果它立刻关闭,重发的 FIN 就没人理,对端会一直重发到超时。
3. 为什么等:旧报文不能污染新连接
- 新旧连接的四元组可能相同,旧连接滞留在网络里的报文,可能被新连接误收。
- 等 2MSL 让旧报文全部消散,新连接才干净。
4. 为什么是两倍 MSL
- MSL 是报文在网络里的最大生存时间。
- 一来一回:主动关闭方最后发的 ACK 最多活 1 MSL;对端重发的 FIN 也最多活 1 MSL。
- 所以等 2 MSL,两个方向上的旧报文都能死干净。
TIME_WAIT 等的不是「礼貌」,是两件事:让自己的最后一个 ACK 有机会重发,让旧连接的报文有时间死透。
4.4 TIME_WAIT 与端口复用
1. 端口为什么不能立即绑
- TIME_WAIT 期间,这条连接的四元组还被内核占着。
- 应用退出后立刻 bind 同一个端口,会失败,报地址已被占用(EADDRINUSE)。
2. 服务器崩溃重启怎么办
- 服务器意外崩溃,重启时想用原端口,被 TIME_WAIT 挡住,不能干等 2MSL。
- setsockopt 的 SO_REUSEADDR 可以让端口在 TIME_WAIT 期间也能绑定。
#include<sys/socket.h>intsetsockopt(intsockfd,intlevel,intoptname,constvoid*optval,socklen_t optlen);3. 靠序列号区分新旧程序
- 立即复用端口的代价是:可能被旧连接的报文污染。
- 新连接的初始序列号是新的,旧报文的序列号对不上。
- 对不上的报文会被丢弃,不会污染新连接的数据。
端口复用是用「可能被污染」换「立即重启」,序列号就是兜底的那道筛子。
五、全篇总结
1. 超时重传靠两个条件判定
- 没收到 ACK 只说明可能丢,要「收不到应答 + 超时」才判定丢包。
- 超时时间以 500ms 为单位动态翻倍,累计到次数就强制关闭连接。
- 序号把确认应答、按序到达、去重三件事一起办了。
2. 连接由内核描述与组织
- 一条连接有四元组、序号、窗口、状态,内核先描述成结构体,再组织成表。
- connect 触发三次握手,由 C 端 OS 自动完成;accept 只取已建好的连接。
3. 三次握手验证全双工
- 两次不够:S 始终确认不了「C 能收」。
- 三次刚好:双方都拿到四个方向的确认。
- 它由四次握手合并而来,S 的 ACK 与 SYN 放进同一个报文。
4. 四次挥手与 TIME_WAIT
- 通常四次,因为 S 的数据没发完,ACK 与 FIN 拆成两个报文。
- 主动关闭方等 2MSL:保证最后的 ACK 能送达、旧报文能消散。
- 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 挂了多久。