IP 层只承诺尽力投递,丢包、乱序、重复、损坏都可能发生。TCP 的”可靠”不是一句口号,而是序列号确认、超时与快速重传、SACK、滑动窗口、拥塞控制这一整套机制协同运转的结果。本文按”确认、重传、流控、拥塞控制”四层递进展开。

它解决什么问题

底层网络对 TCP 有四个”不保证”:不保证送达、不保证按序、不保证不重、不保证内容不变。TCP 要在这之上给应用层一条有序、无重复、无差错、不丢字节的字节流,就必须逐一补上短板:

  • 编号与确认:知道哪些字节到了、哪些没到;
  • 重传:把没到的补上;
  • 流量控制:发送速率不超过接收方的消化能力;
  • 拥塞控制:注入网络的数据不超过网络的承载能力。

还有一个常被误解的边界:TCP 可靠的是字节流,不是应用层的一条条”消息”。消息边界要由上层协议自己定义,这正是粘包、拆包问题的根源。

核心原理

序列号与累积确认

TCP 给字节流里的每个字节编号——序列号是字节序号,不是报文段序号。接收方回复的 ACK 采用累积确认:ACK=n 表示”n 之前的字节我全部收到了,接下来请从 n 开始发”。此外每个报文段都带端到端校验和,校验失败的段直接丢弃、不予确认,等发送方重传。乱序到达的段会被缓存重排,重复的段会被去重。

发送量的双重约束

发送方能向网络中注入多少未确认数据,同时受两个窗口钳制:

  • 接收窗口 rwnd:接收方在 ACK 里通告,反映自己的缓冲区余量;
  • 拥塞窗口 cwnd:发送方自己维护,反映对网络拥塞程度的估计。

实际在途数据量受 min(rwnd, cwnd) 限制——既不压垮对端,也不压垮网络。流量控制管前者,拥塞控制管后者。

深入机制

超时重传:RTO 如何随 RTT 动态估算

超时重传是兜底机制:每发出一段数据就启动定时器,RTO(重传超时时间)内等不到 ACK 就重发。难点在于 RTO 定多少——网络 RTT 时刻波动,固定值不是太小(制造误重传)就是太大(拖慢恢复)。

现代 TCP 按 RFC 6298 的思路估算:对 RTT 样本做指数加权平滑得到 SRTT,再维护一个抖动量 RTTVAR,取 RTO ≈ SRTT + 4×RTTVAR,即”平滑 RTT 加一份抖动余量”。发生超时后 RTO 还会指数退避(逐次翻倍),避免在拥塞时继续向网络倾泻重传流量。另有 Karn 算法的经典约束:重传过的段,其 ACK 不参与 RTT 采样——因为无法判断这个 ACK 对应原始发送还是重传,采进来只会污染估算。

快速重传:三次冗余 ACK

RTO 兜底可靠但太慢。快速重传不依赖计时器,而是利用接收方的重复确认提前发现丢包:

发送方                       接收方
| --- [1000,2000) ---X 丢失
| --- [2000,3000) ----> | 收到乱序段,回 ACK=1000(冗余 ACK 1)
| --- [3000,4000) ----> | 仍缺 1000,回 ACK=1000(冗余 ACK 2)
| --- [4000,5000) ----> | 回 ACK=1000(冗余 ACK 3)
| 触发快速重传,立刻补发 [1000,2000),不等 RTO

累积 ACK 卡在缺口处不动,每收到一个乱序段就重复确认一次旧序号。发送方连续收到 3 个冗余 ACK 即判定该段大概率丢失,立即重传。之所以是 3 个而不是 1 个,是因为短暂乱序也会产生零星冗余 ACK——3 个是”尽快恢复”与”不误判乱序”之间的折中。

SACK:只补真正的缺口

快速重传只能定位最早的缺口;一个窗口内丢了多段时,累积 ACK 说不清哪些区间已经到了。**SACK(选择性确认)**在 ACK 的选项字段里携带已收到的非连续字节区间 [左边界, 右边界),发送方据此只重传缺失部分,避免回退重传。SACK 需在握手时通过 SACK-Permitted 选项协商;受选项长度限制,一个段最多携带 4 个 SACK 块,若同时带时间戳则更少。其扩展 D-SACK 还会报告”哪些段被重复收到”,帮助发送方识别误重传。

滑动窗口流量控制与零窗口

接收方用 rwnd 通告缓冲余量,发送方据此滑动发送窗口。缓冲区打满时通告 rwnd=0,发送方停发。但若随后”窗口已打开”的 ACK 丢失,双方会互相等待形成僵持。TCP 用零窗口探测破局:发送方靠持续计时器定期发 1 字节探测报文,迫使接收方回应当前窗口。

拥塞控制四阶段

拥塞控制围绕 cwnd 展开,经典 Reno 框架分四步:

  1. 慢启动:连接刚建立时不了解网络状况,cwnd 从初始窗口(现代实现常为 10 个 MSS)起步,每收到一个 ACK 增加 1 个 MSS,效果是每个 RTT 翻一倍,指数爬坡。
  2. 拥塞避免:cwnd 达到慢启动门限 ssthresh 后转为线性增长,每个 RTT 约增加 1 个 MSS,小心试探。
  3. 快速重传:收到 3 个冗余 ACK 判定丢包,立即重传缺失段。
  4. 快恢复:将 ssthresh 降为当前 cwnd 的一半,cwnd 置为 ssthresh+3×MSS,每多来一个冗余 ACK 再临时加 1 个 MSS;等到新 ACK 确认重传成功后,cwnd 回落到 ssthresh,进入拥塞避免。
cwnd
| /‾\
| / \ /‾‾‾‾
| / \ / ← 丢包:ssthresh=cwnd/2,快恢复
| / \____/ 后接线性增长
| / 指数(慢启动) 线性(拥塞避免)
+------------------------> 时间

丢包时不回慢启动而只砍一半,依据是冗余 ACK 证明后续段仍能到达,网络没有完全瘫痪。现代默认算法如 CUBIC、BBR 在此框架上做了更精细的窗口调整,但四阶段仍是理解一切的底子。

工程实践

  • 重传率是首要指标:Linux 用 nstat -az TcpRetransSegs 或 ss -ti 查看单连接的重传与 RTT 估算;抓包中大量 TCP Dup ACK、TCP Retransmission 标记意味着丢包在发生。
  • 区分丢包与拥塞:无线误码、路径切换也会丢包,不等于网络拥塞;评估链路别只看吞吐,还要看 P99 RTT 和重传率。
  • 延迟敏感应用关 Nagle:Nagle 算法攒小包,可能与延迟 ACK 互相等待造成几十毫秒停顿,交互式应用常用 TCP_NODELAY 关闭它,代价是小包变多。
  • 别盲目放大缓冲区:过大的 socket buffer 会加剧 bufferbloat,把排队延迟抬高,吞吐未必受益。

面试视角

TCP 靠什么保证可靠传输?

一套组合拳:序列号给字节编号,累积 ACK 报告收货进度,校验和检错丢弃,乱序重排与去重,超时重传兜底、快速重传提速、SACK 精确补缺,再用滑动窗口做流量控制、拥塞窗口做拥塞控制。

快速重传为什么要等 3 个冗余 ACK?

网络乱序也会让接收方偶尔发出冗余 ACK,1 个就重传容易误判。3 个是经验折中:足以过滤大多数乱序干扰,又比等 RTO 快得多。

RTO 是怎么确定的?

不是固定值,而是基于 RTT 样本动态估算:平滑往返时间加上抖动余量,超时后再指数退避。重传段的 ACK 按 Karn 算法不参与采样,避免样本歧义。

流量控制和拥塞控制有什么区别?

流量控制是端到端的,用 rwnd 防止压垮接收方;拥塞控制是全局的,用 cwnd 防止压垮网络。发送方的实际发送窗口取两者较小值。

SACK 解决了什么问题?

累积 ACK 只能说”n 之前都收到了”,无法表达后面乱序到达的区间。SACK 在选项里报告已收到的非连续区间,多段丢失时发送方只补缺口,不做无效的回退重传。

小结

  • 可靠性 = 编号 + 确认 + 重传 + 排序去重 + 校验 + 流控 + 拥塞控制,缺一不可。
  • 重传分三层:RTO 超时兜底、3 个冗余 ACK 触发快速重传、SACK/D-SACK 精确补缺并识别误重传。
  • RTO 随 RTT 动态估算并指数退避;快速重传的”3 次”是乱序与丢包之间的折中。
  • 发送量受 min(rwnd, cwnd) 双重钳制:rwnd 护接收方,cwnd 护网络;拥塞控制沿慢启动、拥塞避免、快速重传、快恢复四阶段循环调节 cwnd。