Appearance
第20章 确认、丢包检测和重传
发送端将 5000 字节数据交给 TCP 协议栈,接收端最终读取到了完整的字节流。如果在网络传输中丢失了某个报文段(Segment),发送端必须重新发送缺失的序列号(Sequence Number)范围,才能保证数据的完整性。本章我们将深入探讨这个决策过程:发送端究竟是如何根据 ACK 确认包、RTT 时间样本以及各类算法,来判断“现在应该触发重传了”的?
先来个预测
假设每个报文段携带 1000 字节数据,发送端连续发送了以下五个报文段:
| 报文段 | Seq 范围 | 网络结果 |
|---|---|---|
| A | 成功到达 | |
| B | 丢失 | |
| C | 成功到达 | |
| D | 成功到达 | |
| E | 成功到达 |
在继续阅读前,不妨先思考三个问题:
- 接收端在收到 C、D、E 时,分别会回复什么 ACK 号?
- 发送端最早会在什么时候重传 B?
- 当重传的 B 最终到达接收端时,累计确认(Cumulative ACK)会直接推进到哪个序列号?
可靠性的最小模型
TCP 发送端会把尚未收到累计确认的数据保留在内存中,这个逻辑集合通常被称为重传队列(Retransmission Queue)。当发送端收到 ACK = 2001 时,意味着接收端已经完整收到了序列号 1001 到 2000 的连续字节流,并且期望接收的下一个字节是 2001。这不仅告诉发送端接收进度,也是发送端清理发送缓存、释放已确认数据的直接依据。
网络中,ACK 确认包本身也可能会丢失。此时累计确认机制就发挥作用了:后续带有更大 ACK 号的确认包,可以“覆盖”之前的进度。例如,如果确认 A 的 ACK 在路上丢失,但随后到达了 ACK = 3001,发送端就会知道 A 和 B 两个报文段都已经成功到达了。因此,TCP 并不要求每一个数据段都必须一一对应一个成功抵达的 ACK。
立即 ACK 与延迟确认(Delayed ACK)
接收端收到数据后,不一定要立刻回复 ACK。它可以稍微等一下,看看是否会有第二个完整的报文段(Full-sized Segment,即 MSS 大小)到达,或者看看是否有反向数据要发送,从而顺便将 ACK “捎带”(Piggyback)过去。这种策略就是延迟确认(Delayed ACK)。
早期的经典 RFC 规范要求:每收到两个完整大小的报文段,至少要回复一个 ACK,并且最大延迟时间不能超过 500 毫秒。但在现代操作系统中,通常会采用更短、更智能的自适应定时器与 ACK 回复频率机制。具体的延迟表现,需要在实际目标平台上进行抓包测量。
当网络出现丢包,导致接收端收到乱序报文(即产生序列号缺口)时,及时反馈情况更有利于发送端快速恢复。RFC 5681 建议:当出现序列号缺口且有新数据到达时,接收端应该立即发送重复确认(Duplicate ACK);同样,当缺口被部分或完全填补时,也应该立即发送 ACK。
回到前面的例子,由于 B 丢失了,接收端在收到 C、D 时,都会立即回复 ACK = 2001。这些重复的 ACK 虽然数值相同,但它们向发送端传递了一个关键信号:“我一直在等 2001,虽然还没等到,但我确实收到了后面的报文。”
延迟确认(Delayed ACK)虽然改变了 ACK 的发送频率和时机,但并没有改变累计确认的底层语义。至于它与 Nagle 算法相遇时可能引发的“死亡延迟”,以及小消息往返场景下为什么要使用 TCP_NODELAY 关闭它,我们将在第29章详细分析。
两条丢包检测路线
经典模型:快速重传(Fast Retransmit)
根据 RFC 5681,经典的快速重传算法将“收到 3 个重复的 ACK”作为网络丢包的确切信号。如果报文段 B 丢失,C、D、E 依次到达接收端,就会连续触发三个 ACK = 2001。当发送端收到这第 3 个重复 ACK 时,它就能确信 B 已经丢了,立刻重传从 Seq 2001 开始的数据。这种机制的好处是,发送端不需要傻等漫长的重传计时器(RTO)超时。
当重传的报文段 B 终于到达接收端时,它填补了序列号缺口。接收端提前在缓冲区里存好的 C、D、E 会立即与 B 拼成一段连续的数据。既然 E 的右边界是 6001,接收端此时会直接回复 ACK = 6001。这种 ACK 序列号的大幅跳跃,被称为累计确认推进。
为什么一定要等 3 个重复 ACK 呢?因为除了丢包,网络中的“乱序到达”或“报文复制”同样会引发重复 ACK。设定“3个”这个经典阈值,是一种稳健的折中方案。在现代实现中,发送端还会结合 SACK(选择性确认)选项来更精确地了解接收端的接收情况。
需要区分的是:快速重传算法只负责决定何时重传数据,而调整发送速率则是拥塞控制(Congestion Control)的任务。例如经典的 TCP Reno 算法在触发快速重传时,会同步更新慢启动阈值(ssthresh)和拥塞窗口(cwnd),我们会在第23章专门计算这些参数的变化过程。
保底机制:重传超时(RTO)
在某些场景下,比如网络中在途的数据报文很少,或者是发送窗口末尾的最后一个报文丢了,发送端根本就凑不齐 3 个重复的 ACK。这时,就只能依靠超时重传(Retransmission Timeout, RTO)来兜底了。
RTO 是根据网络往返时间(RTT)动态计算的。TCP 会持续测量“从发送数据到收到确认”的 RTT 样本,用 SRTT(平滑后的 RTT)来消除短期的网络波动,并用 RTTVAR 来衡量 RTT 的抖动幅度。RFC 6298 定义的核心计算公式如下:
其中,
按照 RFC 6298 规范,如果计算出的 RTO 小于 1 秒,通常会向上取整到 1 秒。当获取到第二个 RTT 样本
在经典规范中,这个结果依然会被拉平到 1 秒。但在真实的生产环境(如现代 Linux 内核)中,往往会采用更高精度的时钟粒度(比如毫秒级),RTO 的下限阈值(RTO_MIN)通常为 200 毫秒,并且还会叠加其他快速恢复机制。因此,你在 Wireshark 抓包中实际观察到的重传间隔,必须结合主机操作系统的具体实现来分析。
另外值得注意的是,一旦发生超时重传,RTO 的值会进行指数退避(Exponential Backoff)。如果网络持续不通,连续重传的间隔时间会成倍增长。
当报文发生重传后,TCP 还会面临一个“身份认证”难题:如果此时收到一个 ACK,它确认的到底是我最初发出的那个包,还是后来重传的包?这就是所谓的“重传歧义”。经典的 Karn 算法直接规定:忽略重传报文的 RTT 样本。但如果在连接建立时协商了 TCP Timestamp 选项,发送端就能利用时间戳精准区分这两者了。
正因如此,你会发现 Wireshark 根据数据包抓取时间算出的 RTT,与服务器内核实际计算并采纳的 RTT,很可能不在同一口径上。
现代丢包检测:RACK 与 TLP
现代 TCP 协议栈(如 Linux 4.4+)通常将 SACK 与 RACK-TLP 算法结合使用。
RACK(Recent Acknowledgment) 彻底改变了思路,它不再去数收到了几个重复 ACK,而是基于时间来判断丢包。RACK 会记录每个数据段的最新发送时间,它的核心逻辑是:如果我发现一个“后发”的报文段已经被确认(无论是累计确认还是 SACK),而一个“先发”的报文段却迟迟没有回音,并且距离其发送时间已经超过了合理的“RTT + 乱序容忍窗口(Reordering Window)”,那么就可以断定这个“先发”段丢了。这种基于时间维度的判定,极大地弥补了经典模型的缺陷,甚至连“重传包本身再次丢失”的复杂场景都能准确识别。
TLP(Tail Loss Probe,尾部丢包探测) 则是为了解决“尾部报文丢失”这个痛点设计的。当发送窗口的最后几个报文丢失,导致 ACK 反馈极其稀少时,传统的做法只能苦等漫长的 RTO 超时。引入 TLP 后,当短暂的探测超时(PTO,Probe Timeout)到期,发送端会主动发出一个探测包(Tail Loss Probe),用来刺激接收端回复最新的 ACK 状态。一旦拿到新的 SACK 反馈,RACK 就能立刻接管并触发快速重传,从而巧妙地避开了慢吞吞的 RTO。当然,传统的 RTO 依然作为最后一道防线保留着。
在实际排障时,仅仅看 pcap 抓包文件,我们只能看到“在某个时刻,网络上冒出了一个重传报文”。至于这个重传究竟是被经典快速重传触发的、被 RACK 标记的,还是由 TLP 探测引起的?这就需要结合发送端的内核指标(Metrics)、TCP 算法配置以及内核日志来综合判定。请记住,Wireshark 界面中显示的 [Fast Retransmission] 或 [Retransmission] 标签,仅仅是抓包软件基于启发式规则推断出来的,不代表内核真实的执行逻辑。
故障排查:如何从抓包中还原恢复时间线?
在分析复杂的丢包重传问题时,最有效的做法是梳理各个数据段的生命周期。建议记录以下核心要素:“发送时间、序列号边界(Seq)、是否被累计确认、是否被 SACK”。
举个例子:
- 帧 101 在 0 ms 发出,Seq 是
。 - 随后帧 102、103、104 在 2 ms、4 ms、6 ms 发出了更高的序列号范围。
- 接着,在 82 ms、84 ms、86 ms,发送端接连收到了三个
ACK = 2001的数据包。 - 仅仅过了 1 毫秒(87 ms),发送端就立刻发出了帧 110,重传了区间
。
这一连串的证据链,完美地吻合了“快速重传”的行为特征。
要验证重传算法,我们还需要计算两个时间差:
- 原发到重传的间隔(这里是 87 ms)。
- 最后一个触发事件到重传的间隔(这里是第三个重复 ACK 到重传,仅需 1 ms)。
根据计算结果进行对症推断:
- 如果重传紧紧跟在第 3 个重复 ACK 之后,经典快速重传是最合理的解释。
- 如果重复 ACK 的数量根本不够 3 个,但较晚发送的数据已经被 SACK 确认,且旧数据段在度过一段“乱序容忍时间”后才被重传,那么这极有可能是 RACK 在起作用。
- 如果在途数据只剩队尾的几个包,且在某个探测周期过后,网络上突然出现了一个探测性质的报文,这就需要考虑是否触发了 TLP。
最后,去寻找那个累计 ACK 号最终“越过”丢失序列号右边界的帧,并计算这段网络恢复的总耗时。
在多源分析中:接收端的 pcap 抓包可以证明数据“真正抵达”的时间;发送端的 ss -ti 命令能告诉你内核当时的真实 rto、重传计数以及正在运行的拥塞控制算法。这三方证据各有侧重:抓包展示了线上飞了什么包,Wireshark 揭示了分析器怎么看,而 ss 输出了内核因为什么而行动。
千万别忘了,ACK 反向路径也必须纳入时间线的考量中。如果是数据成功到达了,但 ACK 在回程中丢失了会发生什么?如果后续的包顺利到达,累计确认会自动推进;但如果发送端先触发了超时重传,网络上就会出现多余的数据副本(Spurious Retransmission)。
如果你有两端的抓包文件,你会发现一种经典的“罗生门”现象:接收端的 pcap 证明“我早就收到了啊”,而发送端的 pcap 则委屈地表示“我没收到你的 ACK 呀”。这种两端视角的差异,正是我们定位单向网络故障的铁证。
受控实验:模拟网络延迟与丢包
为了加深理解,我们可以亲自动手造一个“恶劣”的网络环境。以下命令请在 WSL 或原生 Linux 的 Bash 中运行,你需要具备 root 权限,并安装好 iproute2、iperf3 和 tcpdump。我们将利用 Linux 的网络命名空间(Network Namespace),把这场“混沌实验”安全地限制在一对虚拟网卡内。
bash
sudo ip netns add tcp-rx
sudo ip link add veth-tx type veth peer name veth-rx
sudo ip link set veth-rx netns tcp-rx
sudo ip addr add 10.200.20.1/24 dev veth-tx
sudo ip link set veth-tx up
sudo ip netns exec tcp-rx ip addr add 10.200.20.2/24 dev veth-rx
sudo ip netns exec tcp-rx ip link set lo up
sudo ip netns exec tcp-rx ip link set veth-rx up在终端 A 中,启动 iperf3 服务端:
bash
sudo ip netns exec tcp-rx iperf3 -s -B 10.200.20.2在终端 B 中,启动抓包:
bash
sudo tcpdump -i veth-tx -s 0 -w chapter20.pcap 'tcp port 5201'在终端 C 中,我们使用 tc (Traffic Control) 工具,给发送方向注入 40 ms 的固定延迟和 2% 的随机丢包率,然后发送 32 MiB 数据:
bash
sudo tc qdisc replace dev veth-tx root netem delay 40ms loss 2%
sudo ip netns exec tcp-rx tc qdisc replace dev veth-rx root netem delay 40ms
iperf3 -c 10.200.20.2 -n 32M -i 0.5在数据传输期间,在终端 D 中高频采样发送端的 TCP 内核状态:
bash
watch -n 0.2 "ss -tin dst 10.200.20.2"由于 2% 是随机丢包,每次丢包的位置都不一样,你可以多跑几轮实验。有些轮次你能观察到干脆利落的快速重传,有些轮次则会遇到令人头疼的尾部丢包等待。实验结束后,把 pcap 拖进 Wireshark,用以下过滤表达式进行分析:
text
tcp.port == 5201 &&
(tcp.analysis.duplicate_ack || tcp.analysis.retransmission ||
tcp.analysis.fast_retransmission || tcp.options.sack.count > 0)现象预期与观察方法
- 仔细观察丢包发生后,接收端是不是连续回复了相同的 ACK 号?如果你开启了 SACK 选项,留意 TCP Option 字段中是否携带了已收到的数据区间。
- 注意区分不同的重传表现:有的缺口在几个重复 ACK 后瞬间恢复了;但在途报文很少的时候,重传可能要等上几百毫秒甚至发起主动探测。
- 对照终端 D 的
ss -ti输出,看看发生重传时,内核的rtt、rto、重传计数以及拥塞窗口(cwnd)是如何波动的。(注意:输出字段的丰富程度取决于你使用的 Linux 内核版本)。 - 不要盲信抓包软件:把 Wireshark 标注的
[Fast Retransmission]或[Retransmission]先当做一种参考,随后一定要用 Sequence Number 范围和时间戳差值,亲自在纸上或脑海里复算一遍重传逻辑。
实验完毕后,在服务端和抓包终端按 Ctrl+C 退出。如果你打算一并学习后续的第21至24章,只需要清理本章注入的 tc 丢包规则,网络命名空间可以保留:
bash
sudo tc qdisc del dev veth-tx root 2>/dev/null || true
sudo ip netns exec tcp-rx tc qdisc del dev veth-rx root 2>/dev/null || true如果你已经完成了所有的实验,可以直接执行 sudo ip netns del tcp-rx。删除命名空间后,内核会自动帮你销毁其中的 veth-rx 以及宿主机上的对应虚拟网卡。
如果你的系统内核碰巧没有编译 netem 模块,也没关系。你可以在日常跨主机传输文件时,双开 Wireshark 和 ss -ti。虽然碰不到严重的丢包,但你可以清晰地观察到延迟确认(Delayed ACK)的机制、RTT 的估算过程以及偶尔自然发生的网络抖动。
别小看一趟“风平浪静”的无丢包传输,它的价值在于:为你提供了一个健康网络状态下 ACK 频率和 RTT 的“基准线(Baseline)”。有了这个标尺,日后排查真实故障时你才能做到心中有数。
理解检查小测验
- 发送端连续发送了三个包:
、 、 。假设中间的包在网络中丢失,当第三个包顺利到达接收端时,它回复的 ACK 号应该是多少? - 假设测量到的第一个 RTT 样本为 80 ms,请根据 RFC 6298 的公式,手动计算初始的 SRTT、RTTVAR 以及最终的 RTO 值。
- 当发送窗口末尾只有极少量数据在传输时,如果它们发生了丢包,为什么会导致“3 个重复 ACK”的条件迟迟无法满足?
- 当你在 Wireshark 中看到一条红色的
[tcp.analysis.retransmission]标记时,还需要结合哪些证据,才能确切知道内核触发的是哪种重传算法?
小结
本章中,我们探讨了 TCP 保障数据可靠性的几块核心基石。累计确认提供了接收进度的全局视角,延迟确认(Delayed ACK)优化了控制信令的发送频率;而经典的重复 ACK(快速重传)与 RTO(超时重传)机制,构成了 TCP 丢包检测的基本盘。
针对复杂场景,现代操作系统引入了更敏锐的武器:RACK 突破了“数包”的限制,基于发送时间戳和确认反馈精准识别空洞;TLP 则专门用于“火力侦察”,主动探测尾部丢包死角。在真实的排障过程中,不仅要看抓包报文“发生了什么”,更要结合发送端的底层状态数据,去理解内核“为什么在这一秒扣动重传的扳机”。
规范依据
导航
上一章:第19章 RST、异常断开和半开连接 · 所属篇:第五篇 · 下一章:第21章 乱序、重复数据和 SACK · 教程总览