Skip to content

流量控制与拥塞控制

基于 HTTP 现代标准 · 核于 2026-06

速查

  • 两个「控制」解决两件事:流量控制防止发送方压垮接收方;拥塞控制防止发送方压垮网络。前者是端到端的一对一约定,后者是对整条链路的集体退让。
  • 流量控制靠 rwnd(接收窗口):接收方在每个 ACK 的 16 位 Window 字段里告诉发送方「我还能收多少字节」,发送方据此约束在途数据,本质是滑动窗口
  • 零窗口与窗口探测:接收方缓冲区满时通告 Window = 0,发送方停发并启动持续计时器(persist timer),定期发零窗口探测报文,等待接收方通告新窗口,避免死锁。
  • 拥塞控制靠 cwnd(拥塞窗口):这是发送方自己维护的状态变量(不在报文里),通过观察丢包/ACK 动态估算网络当前能吃多少。
  • 发送窗口 = min(rwnd, cwnd):发送方实际能发多少,取接收方限额与网络限额中的较小者——谁更紧谁说了算。
  • 拥塞控制四阶段:慢启动(指数增长)→ 拥塞避免(线性增长)→ 快重传(收到 3 个重复 ACK 立即重传)→ 快恢复(不回到慢启动,cwnd 减半后继续线性增)。
  • 慢启动并不慢:cwnd 每经过一个 RTT 翻倍(指数增长),到达 ssthresh(慢启动阈值)后转入拥塞避免的线性增长。
  • AIMD(加性增、乘性减):拥塞避免阶段每 RTT 加一个 MSS(加性增),一旦丢包就把 cwnd 大幅砍掉(乘性减),形成经典「锯齿」曲线,是 TCP 公平共享带宽的核心。
  • 算法演进:Reno(经典 AIMD,丢包减半)→ CUBIC(Linux 默认,三次函数增长,丢包减 30%)→ BBR(Google,基于带宽时延建模,不靠丢包判拥塞)。
  • RFC 9293 强制:TCP 端点 MUST 实现慢启动、拥塞避免与指数退避;流量控制与拥塞控制同时生效,缺一不可。

一、一句话分清:护接收方 vs 护网络

初学者最容易把两者搞混,因为它们都「限制发送速度」。但限制的理由完全不同

  • 流量控制(Flow Control):接收方的缓冲区是有限的。如果发送方发得比接收方处理得快,缓冲区溢出、数据被丢弃。流量控制让接收方能「喊停」,保护的是对端这一台机器
  • 拥塞控制(Congestion Control):发送方与接收方之间隔着一整条网络(路由器、链路)。即使接收方处理得过来,中间链路也可能拥塞——路由器队列溢出、丢包、时延飙升。拥塞控制让发送方主动退让,保护的是整条链路这个公共资源

一个比喻

流量控制像「水杯接水」——接水的人(接收方)杯子快满了就喊「慢点倒」。拥塞控制像「高速公路限流」——不管终点停车场多大,路上堵车了所有车都得减速,否则全体瘫痪。发送方实际车速取两者中更慢的那个。

维度流量控制拥塞控制
保护对象接收方(对端缓冲区)网络(中间链路 / 路由器)
核心变量rwnd(接收窗口)cwnd(拥塞窗口)
谁维护接收方通告,发送方遵守发送方自己估算
信息来源报文头 16 位 Window 字段观察丢包、重复 ACK、RTT
触发收缩接收方处理慢、缓冲区将满网络丢包 / 时延变化
是否在报文里传是(显式通告)否(发送方本地状态)

A TCP endpoint MUST implement the basic congestion control algorithms slow start, congestion avoidance, and exponential backoff. ——RFC 9293(TCP 现代规范,2022)

二、流量控制:接收窗口与滑动窗口

TCP 报文头里有一个 16 位的 Window(窗口)字段。RFC 9293 对它的定义是:

The number of data octets beginning with the one indicated in the acknowledgment field that the sender of this segment is willing to accept.

也就是说,接收方在每个 ACK 里都顺带告诉发送方:「从我刚确认的字节开始,我还愿意再收这么多字节。」这个值就是接收窗口 rwnd,它随接收方应用读取数据的快慢而动态变化。

发送方据此实现滑动窗口:窗口内的字节可以连续发出(无需逐个等 ACK),收到 ACK 后窗口向右滑动,腾出空间发新数据。窗口越大,在途(in-flight)数据越多,吞吐越高。

零窗口(Zero Window)与死锁风险

当接收方应用来不及读、缓冲区满了,它会通告 Window = 0,发送方必须立即停发数据。

问题来了:接收方腾出空间后会再发一个「窗口已打开」的通告——但这个通告本身可能丢失。若发送方傻等这个通告、接收方又以为已通知,双方就死锁了。

解决方案是持续计时器(persist timer)+ 零窗口探测:发送方收到零窗口后启动持续计时器,超时就主动发一个零窗口探测报文(zero-window probe,仅含 1 字节或纯探测),逼接收方回一个最新的窗口通告。只要接收方腾出了空间,探测就能问出新的 rwnd,连接得以继续——避免了「通告丢失」导致的永久挂起。

16 位窗口不够用?——窗口缩放选项

16 位 Window 字段最大只能表示 65535 字节。在高带宽 × 高时延的「长肥管道」(如跨洲链路)里,这点窗口远不足以填满带宽。TCP 用 窗口缩放选项(Window Scale,RFC 7323) 在握手阶段协商一个移位因子,把窗口上限扩到 1 GB 量级。这是流量控制能跟上现代高速网络的前提。

三、拥塞控制:拥塞窗口与发送窗口

流量控制只看接收方,完全看不见中间网络的拥堵。于是 TCP 在发送方额外维护一个拥塞窗口 cwnd——它不出现在任何报文里,纯粹是发送方根据网络反馈(丢包、ACK 节奏、RTT)自己估算出来的「网络当前大概能吃多少」。

最终发送方一个 RTT 内能发出的数据量,由下面这个取最小的式子决定:

发送窗口 = min(rwnd, cwnd)
            ↑          ↑
     接收方还能收的   网络还能扛的

谁更紧谁限速:接收方慢就被 rwnd 卡住,网络堵就被 cwnd 卡住。两道闸门同时工作,缺一不可。

为什么 cwnd 不放进报文?

rwnd 是接收方对发送方的明确承诺,必须显式传达。而 cwnd 是发送方对网络的主观猜测——网络不会主动告诉你「我堵了」,发送方只能靠「丢包」这个间接信号去推断。所以 cwnd 天然是发送方的本地状态,不需要、也无法由对端通告。

四、拥塞控制四阶段:cwnd 是怎么变的

TCP 用四个阶段动态调整 cwnd,核心是一句话:没堵就试探着加速,一堵就果断退让。

1. 慢启动(Slow Start)——指数增长

连接刚建立时,发送方不知道网络能吃多少,于是从一个很小的初始窗口(如 10 个 MSS)起步,每收到一个 ACK 就把 cwnd 加一个 MSS——效果上每经过一个 RTT,cwnd 大致翻倍

MDN 的定义点明了它的目的与「试探」本质:

TCP slow start is an algorithm that helps discover the available network bandwidth... The mechanism rapidly increases the volume of information sent from a very low level to a threshold level. ——MDN, Glossary: TCP slow start

「慢启动」一点都不慢

名字容易误导。它叫「慢」是相对「一上来就全速猛灌」而言——起点低;但增长是指数级的,几个 RTT 就能把窗口撑得很大。所谓「慢」指的是谨慎起步,不是增速慢。

当 cwnd 长到慢启动阈值 ssthresh 时,切换到拥塞避免阶段。

2. 拥塞避免(Congestion Avoidance)——线性增长

过了 ssthresh,说明已接近网络容量上限,再翻倍就太冒进了。于是改为每个 RTT 只增加一个 MSS(线性增长),小步试探,逼近而不越过容量天花板。这就是 AIMD 里的「加性增(Additive Increase)」

3. 快重传(Fast Retransmit)

如果发送方连续收到 3 个重复 ACK(对方在说「我还在等某个包」),不必苦等重传定时器超时,立即重传那个疑似丢失的报文。这比等超时快得多,是对「轻度丢包」的快速反应。

4. 快恢复(Fast Recovery)

传统做法是:一丢包就把 cwnd 砍回初始值、重走慢启动——太浪费。快恢复认为「能收到重复 ACK 说明网络还在转发数据,只是偶发丢包」,于是不回慢启动,而是把 ssthresh 设为当前 cwnd 的一半、cwnd 也降到这个水平,然后直接进入拥塞避免的线性增长。这就是 AIMD 里的「乘性减(Multiplicative Decrease)」

cwnd 变化示意(经典 Reno 锯齿曲线)
cwnd
  │              ╱|              ╱|
  │            ╱  |            ╱  |        ← 拥塞避免:线性 +1 MSS/RTT
  │          ╱    |          ╱    |          (加性增 AI)
ssthresh ──╱──────┼────────╱──────┼──
  │       ╱       |×丢包  ╱       |×丢包   ← 丢包:cwnd 砍半
  │      ╱        ▼      ╱        ▼          (乘性减 MD)
  │     ╱ ← 慢启动      ╱
  │    ╱   指数翻倍    ╱
  │   ╱              ╱
  └──┴──────────────┴──────────────────→ 时间(RTT)
     起步           快恢复后从半高继续线性增

慢启动「指数冲」到 ssthresh → 拥塞避免「线性爬」→ 丢包「砍半退」→ 再线性爬……周而复始的锯齿,就是 TCP 在「尽量快」与「不压垮网络」之间的动态平衡。

五、AIMD:为什么这套规则是公平的

AIMD(Additive Increase Multiplicative Decrease,加性增、乘性减) 是上面四阶段的数学内核:

  • 加性增:不拥塞时,每 RTT 把 cwnd 线性加一点(+1 MSS)——温和试探可用带宽。
  • 乘性减:一拥塞,就把 cwnd 成比例砍掉(Reno 砍一半)——剧烈退让,快速给网络让路。

「缓慢加、剧烈减」的不对称设计,让多条共享同一链路的 TCP 连接能收敛到公平、稳定的带宽分配:抢得过头的连接更容易丢包、被砍得更狠,最终大家趋于均衡。这是互联网在没有中央调度的情况下,仍能避免「拥塞崩溃(congestion collapse)」的基石。

六、拥塞算法演进:Reno → CUBIC → BBR

四阶段是「框架」,具体怎么增、怎么减、怎么判拥塞,不同算法各有取舍:

算法年代 / 出处拥塞判据窗口增长丢包后收缩适用场景
Reno / NewReno经典标准丢包(被动)拥塞避免线性 +1/RTTcwnd 砍半教科书基准;低带宽时延积
CUBICRFC 8312,Linux 默认丢包(被动)三次函数,离上次峰值远时猛追、近时缓增cwnd 减 30%高带宽 × 高时延的现代网络
BBRGoogle,2016 起带宽 + RTT 建模(主动)探测瓶颈带宽与最小 RTT,按估算的「管道容量」发送不靠丢包,按模型调速高丢包 / 长肥管道(如视频、跨国)

Reno:经典 AIMD 基准

Reno 是「丢包驱动」的代表:拥塞避免阶段每 RTT 线性 +1,丢包就砍半。Cloudflare 的描述很形象——它的拥塞避免「CWND grows slowly (roughly a full packet per RTT)」,配合慢启动的近似翻倍,形成典型锯齿。缺点是「不丢包就不停加速」(doesn't stop until it sees the packet loss),在高带宽时延网络里爬升太慢、对丢包过度敏感。

CUBIC:Linux 默认,三次函数增长

CUBIC ... became the default congestion control in the Linux kernel. ——Cloudflare, CUBIC and HyStart++ support in quiche

CUBIC 把拥塞避免的窗口增长改成一个以时间为自变量的三次函数:丢包后,窗口先在离上次峰值 Wmax 较远时激进快追,接近 Wmax放缓,越过后再缓慢探测更高带宽——「approaching Wmax aggressively in the beginning... but slowly converging to Wmax later」。它丢包后只减 30%(Reno 是 50%),收缩更温和,因此在长肥管道里吞吐显著优于 Reno。注意:CUBIC 只改了拥塞避免,慢启动逻辑与 Reno 相同(常配合 HyStart++ 优化慢启动退出时机,减少过冲丢包)。

BBR:基于带宽时延,不靠丢包

BBR(Bottleneck Bandwidth and Round-trip propagation time)是 Google 提出的模型驱动算法,思路根本不同:它不把「丢包」当拥塞信号,而是持续测量瓶颈带宽与最小 RTT,据此估算「这条管道的真实容量」,并按这个容量来发送。

BBR 适合什么场景

传统丢包驱动算法在高丢包但不拥塞的链路(如跨国、无线)上会误判——把随机丢包当成拥塞而过度降速。BBR 因为看的是带宽与时延,不被随机丢包欺骗,在视频分发、跨洲传输等场景常有明显吞吐与延迟优势。代价是实现复杂、在与 Reno/CUBIC 混跑时的公平性仍是持续研究的话题。QUIC/HTTP3 等用户态协议栈也在积极采用 BBR 与 CUBIC。

小结

流量控制与拥塞控制是 TCP「不把数据发飞」的两道闸门,分工清晰:流量控制护接收方——靠接收方通告的 rwnd 与滑动窗口,缓冲区满了用零窗口 + 持续计时器探测避免死锁;拥塞控制护网络——靠发送方自维护的 cwnd,经慢启动(指数)、拥塞避免(线性)、快重传、快恢复四阶段,以 AIMD「加性增、乘性减」公平地试探与退让。发送方实际速率永远取 min(rwnd, cwnd),谁更紧听谁的。算法层面,从经典 Reno 到 Linux 默认的 CUBIC(三次函数、减 30%),再到 Google 基于带宽时延建模的 BBR,演进主线是「让 TCP 在越来越快、越来越复杂的网络里跑得更满又不崩」。理解了这两套机制,才能看懂为什么弱网下吞吐会塌、为什么大文件传输需要足够大的窗口——这也直接关系到下一篇要讨论的传输层选型。