tcp-reliability

**TCP 在“不可靠的 IP 网络”之上,靠校验、序号、确认、重传和窗口机制,提供一个“按序、去重、尽力可靠”的字节流服务。** 标准里最核心的基础是:每个字节都有序号;ACK 是**累计确认**,确认号 `X` 表示 `X` 之前的字节都收到了;校验和是强制的;发送端还要维护重传计时器,在没有收到确认时重发。

#status / growing #type / concept

[!info] related notes

tcp-reliability

可以把 TCP 的可靠性先压成一句话:

TCP 在“不可靠的 IP 网络”之上,靠校验、序号、确认、重传和窗口机制,提供一个“按序、去重、尽力可靠”的字节流服务。 标准里最核心的基础是:每个字节都有序号;ACK 是累计确认,确认号 X 表示 X 之前的字节都收到了;校验和是强制的;发送端还要维护重传计时器,在没有收到确认时重发。(datatracker.ietf.org)

先说一个最容易混的点:
流量控制拥塞控制不是一回事。
流量控制关心的是“接收端还吃得下多少”;拥塞控制关心的是“网络现在还能承受多少”。TCP 发送端真正能放进网络里的在途数据量,受两个窗口共同限制:接收端通告窗口 rwnd发送端拥塞窗口 cwnd,实际发送受 min(cwnd, rwnd) 控制。(datatracker.ietf.org)

先把 TCP 可靠性的骨架搭起来

TCP 的可靠性不是靠某一个开关实现的,而是几层机制叠起来:

第一层是差错检测。TCP 校验和覆盖 TCP 头、数据以及伪首部,用来发现传输中的比特错误和部分误投递问题;而且校验和不是可选项,发送端必须生成,接收端必须检查。(datatracker.ietf.org)

第二层是序号与确认。TCP 不是给“包”编号,而是给字节编号;这样接收端就能知道“哪一段字节到了、哪一段没到、哪些是重复的”。标准还规定,ACK 是累计确认,所以发送端可以把“确认号之前的数据”从重传队列里删掉。(datatracker.ietf.org)

第三层是重传。如果某段数据发出去后迟迟没有被确认,发送端要靠 RTO(重传超时) 触发重传;RTO 不是拍脑袋定的,而是根据平滑 RTT(SRTT)和 RTT 波动(RTTVAR)算出来,并且超时后要指数退避。同时,TCP 还会用3 个重复 ACK 触发快速重传,尽量别等超时。(datatracker.ietf.org)

第四层是窗口。窗口一方面决定“现在允许哪些序号的数据飞出去”,另一方面把“接收端承受能力”和“网络承受能力”都纳入发送决策。也就是说,可靠性不是“无限制地重发直到成功”,而是在窗口约束下有节制地发送、确认、重传。(datatracker.ietf.org)

再补一个细节:TCP 还得防“旧连接残留的老包”混进新连接。为此它使用初始序号(ISN)、三次握手、TIME-WAIT 等机制来降低“旧报文被当成新报文”的风险。(datatracker.ietf.org)


一、流量控制:防的是“接收端被撑爆”

流量控制本质上是接收端对发送端说:我现在还能再接收多少字节。TCP 头里有一个 16 位的窗口字段,表示“从 ACK 指向的下一个期望字节开始,我还能接收多少数据”;标准对这个字段的定义就是:发送这个段的一方愿意接收的字节数。发送端看到对方通告的窗口后,就把它当成自己的发送窗口上界。(datatracker.ietf.org)

换句话说,流量控制保护的对象不是链路,不是路由器,而是接收缓冲区。RFC 9293 直接说,段里携带的窗口表示“发送该窗口的一方当前准备接收的序号范围”,并假定这和该连接可用缓冲区有关。窗口报大了,接收端来不及吞,就会丢数据并引发额外重传;窗口报太小,又会把吞吐压得很低。(datatracker.ietf.org)

1)接收窗口到底怎么限制发送

可以把发送端想象成一直维护三段区间:

  • 已经确认的字节
  • 已经发出但还没确认的字节
  • 现在被允许发送的新字节

RFC 9293 对这些变量定义得很清楚:SND.UNA 是最老未确认序号,SND.NXT 是下一个待发送序号,SND.WND 是对端通告的发送窗口;因此新的可发送序号范围就在 SND.NXTSND.UNA + SND.WND - 1 之间。(datatracker.ietf.org)

举个直观例子:
如果接收端通告 rwnd = 64 KB,而发送端已经有 40 KB 数据在途但还没被 ACK,那它眼下最多只能再发大约 24 KB 新数据;不是因为网络一定不行,而是因为接收端只承诺还能再收这么多。这个“剩余额度”就是流量控制的核心。其窗口边界由上面的 SND.UNA / SND.NXT / SND.WND 关系决定。(datatracker.ietf.org)

2)零窗口:接收端忙不过来时怎么办

如果接收端应用层读得太慢,缓冲区被填满,它可以把窗口通告成 0。这时发送端不该再发新数据,但连接也不能就此“僵死”,因为窗口重新打开这件事本身也需要可靠传达。为了解这个死锁,TCP 规定了 Zero-Window Probing:即使窗口为 0,发送端也必须周期性发送零窗口探测;只要接收端还会回复 ACK,连接就要保持打开。(datatracker.ietf.org)

这就是为什么流量控制不是“窗口一变 0,连接就断”。
它真正表达的是:我现在暂时吃不下了,你先别塞;但我们仍保持连接状态,并定期探测我是否恢复。 RFC 9293 还建议零窗口探测的间隔按超时那样指数增大。(datatracker.ietf.org)

3)为什么会有 Window Scaling

TCP 头里的原始窗口字段只有 16 位。这意味着不扩展时,窗口上限就是 2^16 量级;在高带宽×时延(BDP)路径上,这个窗口很容易太小,链路跑不满。RFC 7323 专门定义了 Window Scale 选项,把窗口语义扩展到 30 位,用缩放因子把真实窗口映射到 16 位窗口字段里。(datatracker.ietf.org)

所以今天你看到的“TCP 大窗口”其实不是基本头天然就有,而是靠握手阶段协商的 Window Scale。这个选项只能在 SYN 里协商;如果双方都没有在 SYN 里声明支持,那么这个连接就只能用未缩放窗口。(datatracker.ietf.org)

4)Silly Window Syndrome:为什么“小窗口碎步前进”会很糟

TCP 还有个经典问题叫 Silly Window Syndrome(SWS)。它指的是:接收端每次只腾出一点点空间,发送端每次也只发一点点数据,结果网络上充斥大量极小段,吞吐和效率都很差。RFC 9293 明确要求收发两端都实现 SWS avoidance,并指出 Nagle 算法与发送端 SWS 避免是互补关系。(datatracker.ietf.org)

直觉上看,SWS 说明流量控制不只是“报一个窗口值”这么简单,还要考虑什么时候更新窗口、什么时候值得发送。否则即使“可靠”,效率也会差得离谱。(datatracker.ietf.org)


二、拥塞控制:防的是“网络被灌爆”

如果说流量控制是“别把接收端撑死”,那拥塞控制就是“别把网络挤崩”。RFC 5681 明确规定,慢启动和拥塞避免算法必须用于控制注入网络的未确认数据量;这里的关键变量就是 cwndcwnd 是发送端视角的限制,rwnd 是接收端视角的限制,而真正的数据发送受二者最小值约束。(datatracker.ietf.org)

拥塞控制的核心假设是:丢包通常意味着路径中某处开始拥塞。因此标准里的经典 TCP 拿“丢包”作为主要拥塞信号;如果启用了 ECN,也可以把“被路由器标记拥塞”当信号,而不是非得等到丢包。(datatracker.ietf.org)

1)慢启动:不是慢,而是“从小心试探开始”

慢启动的名字很容易误导。它并不是“线性慢慢加”,而是从一个较小的 cwnd 起步,随着 ACK 到来,每个 ACK 最多让 cwnd 增加一个 SMSS;结果整体上看,大约会以“每个 RTT 翻倍”的速度增长,直到到达 ssthresh 或者观察到拥塞。RFC 5681 把它描述为:TCP 在网络条件未知时要逐步探测可用容量,避免一上来就打出过大的突发流量。(datatracker.ietf.org)

这里可以把慢启动理解成“试探网络胃口”。
网络像黑盒,发送端一开始不知道能吃多少,于是先给少量数据,靠 ACK 回来的节奏建立 ACK clock,再顺着 ACK 节奏往上加速。(datatracker.ietf.org)

2)拥塞避免:从“探路”切到“巡航”

cwnd 增长到 ssthresh 附近后,TCP 不再用慢启动,而切到拥塞避免。这时增长速度显著变慢:RFC 5681 的推荐行为是,大约每个 RTT 把 cwnd 增加 1 个 SMSS,也就是常说的“加性增大”。(datatracker.ietf.org)

直觉上,慢启动像踩油门去找极限;拥塞避免像已经接近极限后,小步试探,看还能不能再挪一点。这样做的目的是:既尽量利用带宽,又别过快把队列灌满。 (datatracker.ietf.org)

3)发生丢包时,为什么有两种反应

TCP 对“丢包”不是一律同一个动作,而是看你是怎么发现丢包的

如果发送端收到了 3 个重复 ACK,RFC 5681 认为这通常意味着“某个段丢了,但后面的段还在继续到达接收端”,说明路径还在工作,只是出现局部丢失。这时发送端要执行 Fast Retransmit:收到第 3 个重复 ACK 后,立即重传看起来丢失的那个段,不用等 RTO 超时。随后进入 Fast Recovery,因为重复 ACK 说明还有数据在流动,ACK clock 并没有完全消失。(datatracker.ietf.org)

如果是RTO 超时才发现没收到 ACK,那情况更严重:说明发送端长时间没有得到有效反馈。RFC 5681 规定,此时 ssthresh 至多降为 max(FlightSize/2, 2*SMSS),而 cwnd 必须降到 1 个满尺寸段 的 loss window,然后重新用慢启动爬回去。(datatracker.ietf.org)

这背后的直觉很重要:
3 个重复 ACK 说明“网络还在转,只是中间掉了一块”;
RTO 超时 则说明“反馈时钟几乎断了”,所以反应必须更保守。(datatracker.ietf.org)

4)RTO 为什么不能拍脑袋设

如果 RTO 太小,会误判正常抖动为丢包,导致大量假重传;太大又会让真正的丢包恢复过慢。RFC 6298 要求 TCP 用 SRTTRTTVAR 来估计 RTO,并在超时后把 RTO 翻倍退避。同时,它要求采用 Karn 算法:对已经重传过的段,不要再拿它们去做 RTT 采样,因为 ACK 到底对应第一次发送还是重传版本会变得含糊。只有使用时间戳选项时,这种歧义才会被消掉。(datatracker.ietf.org)

所以你可以把 RTO 看成 TCP 的“最后保险丝”,而不是平时最常用的恢复手段。理想情况下,很多单个丢包会在重复 ACK 触发的快速重传阶段就被修掉;RTO 更像“ACK 时钟都失真了”的兜底。(datatracker.ietf.org)

5)SACK 为什么重要

普通 TCP 的 ACK 是累计确认。这意味着如果中间丢了一段,即便后面的好几段都收到了,接收端也只能反复 ACK“我还在等那个缺口左边的下一个字节”。RFC 2018 指出,在一个窗口里出现多个丢包时,单靠累计 ACK 会严重伤吞吐,因为发送端可能得一个 RTT 一个 RTT 地摸索,甚至重传一些其实已经收到的数据。(datatracker.ietf.org)

SACK 的作用就是:接收端除了累计 ACK 左边界外,还能额外告诉发送端“这些非连续块我也已经收到了”。这样发送端就能更精准地只重传真正缺失的段。RFC 5681 也明确提到,很多增强型丢失恢复算法是建立在 SACK 基础上的。(datatracker.ietf.org)

6)ECN:不丢包也能传达“快堵了”

经典 TCP 多半靠丢包来感知拥塞,但 RFC 3168 定义了 ECN:支持 ECN 的路由器在队列接近拥塞时,可以在 IP 头里设置 CE 标记,而不是直接丢包;随后接收端通过 TCP 里的 ECE 通知发送端,发送端再用 CWR 告诉对方“我已经收缩拥塞窗口了”。(datatracker.ietf.org)

它的意义是:如果网络能更早、更温和地告诉你“我快满了”,TCP 就不必总靠“真的丢了包”才后退。这对降低延迟和减少无谓重传都很有帮助。(datatracker.ietf.org)


三、流量控制和拥塞控制是怎么一起工作的

最关键的一句话再说一遍:

发送端最终能发多少,不是只看 rwnd,也不是只看 cwnd,而是看 min(cwnd, rwnd) RFC 5681 明确就是这么规定的。(datatracker.ietf.org)

这意味着会有两种完全不同的“限速来源”:

一种是接收端限速
例如接收程序读得慢,rwnd 从 256 KB 掉到 8 KB;哪怕网络一点也不堵,发送端也只能按这 8 KB 左右的在途额度发。这个场景里性能瓶颈是接收方处理能力,不是网络。其基础就是接收端通过窗口字段通告还能接收多少数据。(datatracker.ietf.org)

另一种是网络限速
例如接收端一直给 1 MB 窗口,但中间链路开始掉包,发送端的 cwnd 因拥塞被压到 16 KB;这时真正限制吞吐的是拥塞控制,不是接收缓冲。其依据就是 RFC 5681 对 cwnd 的定义和 min(cwnd,rwnd) 规则。(datatracker.ietf.org)

再换句话说:

  • rwnd 小:说明“你别发太多,我来不及收
  • cwnd 小:说明“你别发太多,路上快堵了

这俩都可能让 TCP 变慢,但根因完全不同。(datatracker.ietf.org)


四、把 TCP 可靠性连成一条完整链路

你可以把一次正常 TCP 发送理解成下面这个闭环:

发送端把字节流切成段,给每个字节分配序号;
接收端按序号判断哪些字节到了、哪些还没到,并发累计 ACK;
发送端根据 ACK 前移 SND.UNA,把已确认段从重传队列移走;
如果 ACK 长时间不来,就由 RTO 触发重传;
如果后续段到了但中间缺了一块,重复 ACK 会帮助发送端更早进入快速重传;
整个过程中,发送端还要同时受接收端窗口和拥塞窗口约束。(datatracker.ietf.org)

所以 TCP 的“可靠”不是某种神秘保证,而是一整套反馈回路:
校验发现损坏,序号定位缺口,ACK 反馈进度,RTO 和重复 ACK 触发修复,rwnd 保护接收端,cwnd 保护网络。 (datatracker.ietf.org)


五、几个最容易混淆的误区

误区 1:有 ACK 就等于“每个包都单独确认”。
不是。标准 TCP 的 ACK 是累计确认;这也是为什么中间丢一段时,后面的到达通常会表现为重复 ACK,而不是“每段都独立确认成功”。SACK 是在累计 ACK 之外补充“哪些离散块也到了”。(datatracker.ietf.org)

误区 2:流量控制就是拥塞控制。
不是。流量控制保护接收端缓存,拥塞控制保护网络路径;二者信号来源、调整目标、变量含义都不同。(datatracker.ietf.org)

误区 3:只要 rwnd 很大,就一定能跑满带宽。
也不是。若 cwnd 因丢包/ECN 被压小,或者路径 RTT 很大而窗口扩展没协商好,吞吐仍可能上不去。RFC 7323 专门为高带宽×时延路径定义了窗口扩展,就是因为原始 16 位窗口可能不够。(datatracker.ietf.org)

误区 4:超时和 3 个重复 ACK 只是两种写法。
不是。它们在 TCP 语义里代表的严重程度不同:重复 ACK 说明路径还在流动,只是有缺口;RTO 说明反馈显著缺失,因此 RFC 5681 对超时后的 cwnd 收缩更激烈。(datatracker.ietf.org)


一句话总结

TCP 的可靠性,本质上是“字节级序号 + 累计确认 + 必要时重传 + 双窗口约束”的反馈系统。
其中:

  • 流量控制解决“接收端来不及收”的问题,核心变量是 rwnd
  • 拥塞控制解决“网络来不及运”的问题,核心变量是 cwnd
  • 真正的发送上限是 min(cwnd, rwnd) (datatracker.ietf.org)
创建于 2026/3/13 更新于 2026/5/27