题干与适用场景
发送方连续发送五个 1000 字节的 TCP 数据段,起始序列号为 1000,其中第二段丢失,后三段到达接收方。请推导 ACK、SACK 和重传过程,并比较超时重传、快速重传及现代丢包检测的适用边界。
题目默认连接已经建立,五段都携带数据,接收窗口与拥塞窗口足以让它们在途,接收方能暂存乱序数据,并且双方在握手时协商了 SACK。序列号按字节计数,区间使用左闭右开表示。若取消 SACK,累计 ACK 与经典快速重传仍然成立,只是发送方少了哪些高序字节已经到达的信息。
这道题适合后端、基础架构、SRE、客户端、网络和通用软件工程岗位。核心能力是从传输协议状态推导结果并验证现场证据,因此归为 general;它不考 HTTP/3 的跨流隔离,也不考连接建立、关闭和 TIME_WAIT。
面试官考察点
第一,能否说准序列号和 ACK 的单位。TCP 给字节编号;一个段的序列号是它第一个数据字节的编号,累计 ACK 表示接收方下一个期望字节,而不是“收到了第几个包”。
第二,能否沿时间线推导。第二段丢失后,后续乱序段不会推动累计 ACK,但会触发重复 ACK;第三个重复 ACK 是 RFC 5681 经典快速重传的丢失信号。把最初确认第一段的 ACK 也算作重复 ACK,会提前一拍得出错误结论。
第三,能否区分恢复机制。RTO 处理没有足够 ACK 反馈的丢失;快速重传利用后续数据产生的重复 ACK 提前恢复;SACK 描述已经收到的非连续字节块,帮助发送方避免重传已到数据,但不替代累计 ACK,也不单独决定拥塞窗口。
第四,能否解释不确定性。重复 ACK 也可能来自重排或复制,超时也可能是 RTT 突增而非真实丢包。可靠传输、丢失检测与拥塞控制相互作用,但不是同一个概念。
第五,能否越过教材口号。强回答会说明小在途量和尾部丢失可能凑不够三次重复 ACK,并指出 RACK-TLP 等实现可以用发送时间与 SACK 反馈减少对固定包数阈值和 RTO 的依赖。
回答前需要澄清的问题
- 序列号和长度是否按字节给出? 若题目给的是相对包号,先转换为字节区间;SYN 和 FIN 也占序列空间,但本题只有已建立连接中的数据段。
- SACK 是否在握手时协商? 未协商时只能依靠累计 ACK 和所用恢复算法;协商后,重复 ACK 可以携带已收到的乱序块。
- 在途量是否足以产生三个重复 ACK? 中间段后至少还要有三个新段到达,经典快速重传才有足够反馈;尾段丢失通常没有后续数据制造重复 ACK。
- 讨论 RFC 5681 的经典算法还是某个实际内核? 经典阈值便于推导,真实系统可能使用可配置阈值、SACK 恢复、RACK-TLP 或其他扩展,抓包结论必须结合操作系统与版本。
- 问题要求协议正确性还是性能诊断? 前者说明最终如何按序交付;后者还要观察 RTT、RTO、拥塞窗口、重排、捕获位置和网卡卸载。
30 秒回答框架
“TCP 的序列号按字节计数,累计 ACK 是下一个期望字节。第一段覆盖 1000 到 1999,接收后返回 ACK 2000。第二段 2000 到 2999 丢失;后三段虽然到达并可暂存,但缺口仍从 2000 开始,所以每到一段都再次返回 ACK 2000。若协商了 SACK,这些 ACK 还会报告已经收到的 3000 到 6000。
按 RFC 5681 的经典路径,第三个重复 ACK 触发对 2000 到 2999 的快速重传,无需等待 RTO。缺口补齐后,累计 ACK 一次前进到 6000。若在途数据太少、尾段丢失或 ACK 反馈中断,发送方可能只能等 RTO;RTO 根据平滑 RTT 与 RTT 波动计算,超时后还会指数退避并更强地收缩拥塞窗口。SACK 提高多丢包恢复精度,现代 RACK-TLP 还能用发送时间、SACK 和探测包更快识别尾部或重传丢失。抓包时我会同时检查累计 ACK、SACK 块、重传间隔和实际协议栈,避免把重排或分析器标签直接当成丢包证明。”
分步骤深入解答
第一步:先固定字节区间与累计 ACK
五段的逻辑字节范围如下:
A: SEQ=1000, LEN=1000 -> [1000, 2000) 到达
B: SEQ=2000, LEN=1000 -> [2000, 3000) 丢失
C: SEQ=3000, LEN=1000 -> [3000, 4000) 到达
D: SEQ=4000, LEN=1000 -> [4000, 5000) 到达
E: SEQ=5000, LEN=1000 -> [5000, 6000) 到达A 到达后,接收方已经连续拥有 1000 到 1999,因此返回 ACK=2000。这个 ACK 首次推进确认边界,是新 ACK,不是重复 ACK。
B 丢失后,C 虽然在接收窗口内,却无法填补 2000 开始的缺口。接收方可以缓存 C,但累计确认仍为 ACK=2000。D、E 同理。RFC 9293 把 ACK 字段定义为接收端下一个期望序列号,这解释了为什么 ACK 不会跳到每个乱序段的末尾。
第二步:推导三次重复 ACK 与快速重传
在经典 RFC 5681 路径中,C、D、E 每到一段都会让接收方立即返回一个重复 ACK=2000:
收到 A -> ACK 2000 新 ACK
收到 C,B 有缺口 -> ACK 2000 + SACK [3000,4000) 第 1 个重复 ACK
收到 D,B 有缺口 -> ACK 2000 + SACK [3000,5000) 第 2 个重复 ACK
收到 E,B 有缺口 -> ACK 2000 + SACK [3000,6000) 第 3 个重复 ACK
发送方重传 B -> SEQ 2000, LEN 1000
接收方收到 B -> ACK 6000 缺口补齐,累计前进第三个重复 ACK 到达时,发送方推断 B 很可能丢失并快速重传 [2000,3000),不用等待重传计时器到期。接收方此前已缓存 C、D、E,所以 B 一到,连续字节范围立刻扩展到 5999,累计 ACK 可以直接从 2000 前进到 6000。
“三个重复 ACK”是丢失启发式,不是数学证明。网络重排会让高序段先到,同样产生重复 ACK;复制的报文或 ACK 也可能制造该信号。阈值在快速恢复与误判重排之间取折中,实际协议栈可以采用额外算法。
第三步:说明 SACK 提供什么、没有提供什么
累计 ACK 只能告诉发送方“2000 之前连续收到”。SACK 选项可以再报告接收缓冲中非连续的字节块,例如 [3000,6000)。它解决的是信息缺失:发送方知道高序数据已经到达,恢复多个缺口时可以跳过这些块。
SACK 有三个边界:
- 必须在 SYN 阶段通过 SACK-Permitted 协商,之后接收方才在 ACK 中携带 SACK 块。
- SACK 不会把累计 ACK 从 2000 推到 6000;只有
[2000,3000)的缺口补齐,累计边界才前进。 - SACK 是接收方给出的建议性信息。它帮助发送方维护恢复记分板,具体重传顺序与拥塞响应仍由发送端算法决定。
若 B 和 D 都丢失,单靠一次快速重传只能先修 B;没有 SACK 的经典恢复更难判断 D 是否已到。SACK 会报告 C 与 E 的不连续块,使发送方更精确地定位两个缺口,避免把已缓存的 C、E 一并重传。
第四步:解释 RTO 为什么仍然不可缺少
快速重传依赖 ACK 反馈。若只发送 A、B,且 B 是尾段,B 丢失后没有 C、D、E 触发重复 ACK;若返程 ACK 也中断,发送方同样得不到三次重复 ACK。此时重传计时器提供最终保障。
RFC 6298 维护平滑往返时间 SRTT、往返时间波动 RTTVAR 和时钟粒度 G:
首次 RTT 样本为 R:
SRTT = R
RTTVAR = R / 2
RTO = SRTT + max(G, 4 * RTTVAR)
后续样本 R':
RTTVAR = (1 - 1/4) * RTTVAR + 1/4 * abs(SRTT - R')
SRTT = (1 - 1/8) * SRTT + 1/8 * R'
RTO = SRTT + max(G, 4 * RTTVAR)例如首次样本 R=120 ms,且 G 不超过 240 ms,则 SRTT=120 ms、RTTVAR=60 ms,公式原值为 360 ms。RFC 6298 建议将小于 1 秒的 RTO 向上取到 1 秒;尚未获得 RTT 样本时也建议初始 RTO 为 1 秒。真实内核可能有更新算法和实现差异,所以不能仅凭“不是整整 1 秒”否定超时重传。
计时器到期后,发送方重传最早的未确认数据,并把 RTO 加倍后重新计时。指数退避避免在路径持续拥塞或断开时反复注入数据。对重传段直接采 RTT 会产生“这个 ACK 确认原段还是重传段”的歧义;没有时间戳消除歧义时,应遵守 Karn 算法,不使用该样本更新 RTT。
第五步:把可靠性恢复与拥塞响应分开
重传回答“怎样补回缺失字节”,拥塞控制回答“之后允许多少数据在途”。两者由同一丢失信号触发,但不能混为一谈。
RFC 5681 的经典处理将 RTO 视为更严重的信号:ssthresh 不高于 max(FlightSize/2, 2*SMSS),cwnd 降到不超过一个满尺寸段,再从慢启动恢复。三次重复 ACK 表明后续段仍在到达、ACK 时钟尚存,发送方进入快速重传与快速恢复,收缩窗口但不执行同样强烈的 RTO 重置。
还要区分流量控制。接收方通告的 rwnd 保护接收缓冲,发送方的 cwnd 保护网络,实际可发送上限同时受两者约束。SACK 描述接收状态,不会扩大 rwnd 或 cwnd。
第六步:补上现代 RACK-TLP 的边界
固定三次重复 ACK 对短流、尾部丢失、重传再次丢失和较强重排并不理想。RFC 8985 将 RACK-TLP 推荐为传统重复 ACK 计数的替代方案:RACK 结合每段最近发送时间、RTT 与 SACK 反馈,以时间顺序推断更早发送的段是否丢失;TLP 在 ACK 反馈稀少的尾部发送探测,尽量在 RTO 前恢复 ACK 时钟。
因此,面试中应先准确解释经典机制,再加一句实现边界:真实抓包可能在不足三次重复 ACK 时出现恢复,也可能出现尾部探测;应核对协议栈算法和版本。把所有实现都描述成“严格等三次重复 ACK,否则一定等 RTO”会把教材模型误当成完整现状。
第七步:用抓包证据验证,而不是只看重传计数
验证时至少建立一条可对齐的时间线:
- 在发送端与接收端同时捕获,确认原段在哪一侧消失,排除只在单一观察点不可见。
- 对齐
SEQ、LEN、累计ACK和 SACK 块,确认是否真的存在字节缺口。 - 数从上一个新 ACK 之后到来的重复 ACK,不能把基线 ACK 算进去。
- 比较重传与第三个重复 ACK 或 RTO 的相对时间,并核对拥塞窗口、RTT 和协议栈算法。
- 结合应用延迟和接口丢包、重排、队列指标,避免只凭一个 Wireshark 分析标签定根因。
Wireshark 的 tcp.analysis.fastretransmission、tcp.analysis.retransmission 和 tcp.analysis.duplicateack 是分析器根据当前捕获推断的结果,不是 TCP 头里的标志位。TSO、GRO 等卸载还可能让主机抓包看到的段边界不同于线上报文。重要结论应由双端序列空间与时间证据交叉确认。
高质量示范回答
“我先按字节序列推导。A 的 SEQ=1000、长度 1000,所以收到 A 后,接收方下一个期望字节是 2000,返回的新 ACK 是 2000。B 覆盖 2000 到 2999 且丢失。C、D、E 虽然分别覆盖 3000 到 5999,且已经到达,但它们不能填上 2000 的缺口,因此累计 ACK 一直停在 2000。每个乱序段会产生一个重复 ACK;如果握手协商了 SACK,接收方还会逐步报告已经缓存的 3000 到 6000。
按 RFC 5681 的经典算法,C、D、E 造成三个重复 ACK 2000。第三个到达后,发送方快速重传 B,不等 RTO。B 到达时,接收缓冲中的数据变成连续区间,累计 ACK 一次跳到 6000。这里最容易数错的是把 A 后面的第一个 ACK 2000 当成重复 ACK;它其实是推进确认边界的新 ACK。
如果丢的是尾段或在途量不足,就没有三个后续段制造重复 ACK,最终保障是 RTO。RTO 由平滑 RTT 加四倍 RTT 波动估算,RFC 6298 还规定超时后指数退避。RTO 通常伴随比快速恢复更强的拥塞窗口收缩,因为 ACK 时钟可能已经消失。SACK 的作用是告诉发送方哪些不连续高序字节已收到,特别有利于多个缺口;它不会自己推进累计 ACK,也不是拥塞控制算法。
真实系统还可能使用 RACK-TLP,按发送时间和 SACK 反馈识别丢失并探测尾部,所以三次重复 ACK 是必须掌握的经典推导,不是所有抓包唯一可能的触发器。排查时我会在双端对齐序列号、长度、累计 ACK、SACK 块和重传时间,核对协议栈与卸载设置,再区分真实丢包、重排、ACK 路径问题和分析器误判。”
常见错误
- 把 ACK 当成包号 → TCP 确认的是连续字节空间 → 用
SEQ + LEN推导下一个期望字节。 - 把 A 后的 ACK 2000 算作第一个重复 ACK → 它首次推进累计确认 → 从其后相同 ACK 号且不推进边界的 ACK 开始计数。
- 说收到 C 后返回 ACK 4000 → B 的字节缺口仍在 → 累计 ACK 保持 2000,并用 SACK 报告 C。
- 说 SACK 取代累计 ACK → TCP 仍以累计 ACK 推进连续边界 → 把 SACK 当作额外的非连续块信息。
- 说三个重复 ACK 证明丢包 → 重排和复制也可产生相同信号 → 称其为经典丢失启发式,并核对时间线。
- 说任何丢包都能快速重传 → 尾部或小在途量可能没有足够重复 ACK → 保留 RTO,并说明 RACK-TLP 的改进。
- 把重传与拥塞控制当成一件事 → 补数据与限制发送速率解决不同问题 → 分别说明恢复动作、
cwnd与rwnd。 - 把 Wireshark 标签当成线上的协议位 → 标签由捕获上下文推断且可能受卸载影响 → 用双端序列空间和时序交叉验证。
追问及应对
追问一:如果只发两个段,第二段丢失,会发生快速重传吗?
经典路径不会,因为没有更高序数据到达接收端,也就没有三个重复 ACK。发送方通常等待 RTO;采用 RACK-TLP 的协议栈可能发送尾部探测以争取 ACK 反馈,但探测失败后仍需 RTO 兜底。
追问二:如果 C 只是重排,B 稍后自己到达,会怎样?
接收 C 会产生重复 ACK 2000 和相应 SACK。若 B 在达到丢失阈值前到达,累计 ACK 会前进,避免重传;若重排足够深而先触发经典三次重复 ACK,可能出现伪快速重传。RACK 使用时间型重排窗口来降低固定包数阈值在这类路径上的误判。
追问三:B 和 D 同时丢失,为什么 SACK 更有价值?
接收方可报告已收到的 C 与 E 块,发送方据此维护缺口记分板,跳过已 SACK 的范围并恢复 B、D。没有 SACK 时,累计 ACK 只暴露最左缺口,经典 Reno 对一个窗口内多个丢失的恢复信息更少,可能需要部分 ACK 或最终等到 RTO。
追问四:首次 RTT 样本是 120 ms,为什么不能把 RTO 直接设成 120 ms?
RTT 会受排队和路径变化影响。RFC 6298 首次将 RTTVAR 设为 R/2;在 G 不超过 4 × RTTVAR 时,原始 RTO 为 R + 4 × R/2 = 3R。本例为 360 ms,并建议向上取到至少 1 秒。直接使用单次 RTT 会让轻微抖动触发伪超时和无谓重传。
追问五:抓包只看到重传,没有三个重复 ACK,能否认定是 RTO?
不能。捕获可能漏掉返程 ACK,观察点可能在卸载前后,协议栈也可能使用 RACK-TLP 或其他恢复算法。先比较重传与上一条 ACK 的时间、检查 SACK 与尾部探测、核对发送端内核状态,并用接收端捕获补全证据。
追问六:为什么超时后的拥塞响应通常比快速恢复更强?
三次重复 ACK 说明后续段仍从网络中离开并到达接收端,ACK 时钟尚在;RTO 则可能表示整批在途数据或反馈路径都没有进展。经典 RFC 5681 因此在超时后把 cwnd 降到不超过一个满尺寸段并重新慢启动,而快速恢复保留一个缩小后的发送窗口。