第一次做 2022 年那道拥塞控制的题,我看到「拥塞窗口从 16KB 超时后,再涨回 16KB 至少要几个 RTT」,条件反射写了「翻倍嘛,4 个 RTT 就够」。
结果答案是 11 个 RTT。
错在哪?我只记得「慢开始每 RTT 翻倍」,却忘了翻倍只在 cwnd 小于门限时才成立,一旦超过门限就变成「每 RTT 只加 1」的线性爬升。这两个阶段混为一谈,算出来就差三倍。
这篇把流量控制和拥塞控制两条线彻底分开讲,重点是那张 cwnd 变化曲线,以及超时和 3 个重复 ACK 两种「掉下去」的方式到底差在哪。
〇、先花两分钟:流量控制 vs 拥塞控制
这两个词长得像,但管的是两件事,用两个类比一下就懂:
-
流量控制(flow control) :你往我这边发数据,我处理不过来了,我就告诉你「慢点发」。这是点对点的------只涉及你和我这一对收发方,目的是让接收方来得及收。
-
拥塞控制(congestion control) :整条网络都堵车了,大家都在狂发数据,路由器缓存爆满开始丢包。这时候不是「你和我」的问题,而是全局的------目的是别让过多数据一股脑灌进网络,把路压垮。
一句话记法:流量控制是「我方吃不消,让你慢点」;拥塞控制是「路上堵车了,大家都要慢」。
实际发送时,发送方的窗口要同时受两者约束,取较小值:
发送窗口上限 = min(接收窗口 rwnd, 拥塞窗口 cwnd)
rwnd 是接收方说了算(我还能收多少),cwnd 是网络说了算(路上还能扛多少)。下面分两节,先讲 rwnd 怎么管流量,再讲 cwnd 怎么管拥塞。
图示:cwnd 随 RTT 的变化曲线------先指数上升(慢开始)到门限 ssthresh,再线性上升(拥塞避免),超时后骤降为 1,如此循环;并标注 ssthresh 切换点
一、发送窗口 = min(rwnd, cwnd):两个窗口各管什么
TCP 是全双工,通信双方各自维护自己的发送窗口和接收窗口。谈窗口必须先说清「是谁的窗口」:
| 窗口 | 由谁决定 | 限制什么 | 单位 |
|---|---|---|---|
接收窗口 rwnd |
接收方(写在报文段「窗口」字段里通告) | 发送方别发太快,我收不过来 | 字节 |
拥塞窗口 cwnd |
发送方(自己估算网络状况) | 别往网络里灌太多,防拥塞 | 字节(常换算成报文段个数) |
报文段首部的「窗口」字段,填的是本方的接收窗口 rwnd ,不是发送窗口。发送方收到后,拿它和自己的 cwnd 取小,得到实际能发的发送窗口。
⚠️ 易错:窗口字段是「我方还能收多少」,不是「我方还能发多少」;它是接收窗口,不是发送窗口。
二、流量控制:rwnd 怎么让发送方慢下来
机制就是第 5 章讲过的滑动窗口 (以字节为单位)。连接建立时,接收方通告一个 rwnd,发送窗口 ≤ rwnd。接收方每确认一批数据,就带着最新的 rwnd 回来,发送窗口随之滑动。
教材经典的三级递减例子:接收方通告 rwnd 从 400 → 300 → 100 → 0,发送方的发送窗口同步缩小,最后被「卡住」不允许再发。
零窗口死锁 :接收方发了 rwnd=0 后,自己腾出了空间,又发了一个「rwnd=400」的报文------但这个报文在路上丢了。于是:发送方一直等非零窗口通知,接收方一直等发送方的数据,双方死锁。
解决办法:持续计时器(persistence timer) 。发送方一收到零窗口,就启动这个计时器;超时后发一个零窗口探测报文段(只带 1 字节数据),对方回确认时顺便带上当前窗口值。如果还是 0 就重设计时器,非 0 就打破死锁。
⚠️ 易错 :窗口为 0 时不是彻底静默------零窗口探测段、纯确认段、带紧急数据的段仍然能发。
三、拥塞控制一般原理:cwnd 和 ssthresh 是什么
发送方维持一个 cwnd(拥塞窗口)。假定对方接收能力足够大(rwnd 很大),那么发送窗口就 ≈ cwnd,发送方靠调节 cwnd 来控制注入网络的速率。
还有个关键变量 慢开始门限 ssthresh,它决定切换点:
-
cwnd < ssthresh→ 用慢开始(指数增长) -
cwnd > ssthresh→ 用拥塞避免(线性增长) -
cwnd = ssthresh→ 两者皆可
记住这条总纲,下面四种算法就只是「在哪些点怎么改 cwnd 和 ssthresh」。
四、慢开始:每 RTT 翻倍(指数增长)
cwnd 从一个很小的值起步(老标准 1~2 个报文段,新 RFC 5681 为 2~4 个 SMSS)。每收到一个对新报文段的确认 ,cwnd 最多 +1 个 SMSS。
因为每经过一个 RTT,这一轮发出的报文段都会回来确认,相当于 cwnd 在每个 RTT 翻倍 。所以慢开始阶段是指数增长。
⚠️ 名字陷阱 :「慢开始」的「慢」只指起步小(先发 1 个试探),不是说增长慢------它其实是指数级暴涨,是所有阶段里涨得最快的。
五、拥塞避免:每 RTT +1(线性增长)
当 cwnd 超过 ssthresh,切到拥塞避免。这一阶段 cwnd 每个 RTT 只加 1 个报文段,线性缓慢爬升。
「避免」不是「完全不发生拥塞」,只是把增速从指数压成线性,让网络慢慢试探上限,别一下冲爆。
六、慢开始门限 ssthresh 的切换点
ssthresh 是「从翻倍改成 +1」的拐点。推演时每一步都要先比 cwnd 和 ssthresh:
-
还没到门限:
cwnd = cwnd × 2(慢开始) -
过了门限:
cwnd = cwnd + 1(拥塞避免) -
正好等于门限:按惯例这一轮仍可算慢开始最后一次翻倍,下一轮起转拥避(具体看题设,两种处理分差很小,关键是别把「过门限后继续翻倍」)
七、超时怎么办:cwnd 骤降为 1,重来慢开始
一旦超时(说明网络很可能严重拥塞):
ssthresh = cwnd / 2,cwnd = 1
然后从 cwnd=1 重新开始慢开始。注意:超时是「最严重」的信号,cwnd 直接被打回 1,前面攒的窗口全废。
八、快重传:一连 3 个重复 ACK 立即重传
接收方收到失序报文段时,会立刻 发重复确认(不是等超时)。发送方只要一连收到 3 个重复确认 ,就推断只是丢了个别报文段(网络没那么糟),不等超时,立即重传那个缺失的段。这能把吞吐量提大约 20%。
九、快恢复:cwnd = ssthresh,不置 1(与超时的关键区别)
收到 3 个重复 ACK(不是超时)时,处理比超时温和:
ssthresh = cwnd / 2,cwnd = ssthresh(然后进入拥塞避免,线性增长)
⚠️ 最核心的易错点 :超时 → cwnd=1;3 个重复 ACK → cwnd=ssthresh(不置 1 )。二者门限都是 cwnd/2,但掉下去的「坑」深浅完全不同。考试就爱在这里挖坑,让你把 3-ACK 也写成 cwnd=1。
十、AIMD:把四种算法一条线串起来
TCP Reno 的拥塞控制本质是 AIMD(加法增大、乘法减小):
-
加法增大(AI) :正常时每 RTT
+1(拥塞避免阶段) -
乘法减小(MD) :一出问题就把
ssthresh设为cwnd/2(无论是超时还是 3-ACK)
四种算法的映射:
| 算法 | 触发 | 对 ssthresh | 对 cwnd |
|---|---|---|---|
| 慢开始 | cwnd < ssthresh | 不变 | 每 RTT ×2 |
| 拥塞避免 | cwnd > ssthresh | 不变 | 每 RTT +1 |
| 超时 | 超时 | = cwnd/2 | = 1 |
| 快重传/快恢复 | 收到 3 个重复 ACK | = cwnd/2 | = ssthresh |
一句话:平时线性 +1,出问题门限减半;超时最狠掉到 1,3-ACK 只掉到门限。
十一、cwnd 数值推演算例(自编,单位:报文段)
初始 ssthresh = 16,cwnd = 1(慢开始)。约定 cwnd = ssthresh 时转拥塞避免。
| RTT | 阶段 | cwnd | ssthresh | 说明 |
|---|---|---|---|---|
| 0 | 慢开始 | 1 | 16 | 初始 |
| 1 | 慢开始 | 2 | 16 | ×2 |
| 2 | 慢开始 | 4 | 16 | ×2 |
| 3 | 慢开始 | 8 | 16 | ×2 |
| 4 | 慢开始→拥避 | 16 | 16 | 到门限,转拥避 |
| 5 | 拥避 | 17 | 16 | +1 |
| 6 | 拥避 | 18 | 16 | +1 |
| 7 | 拥避 | 19 | 16 | +1 |
| 8 | 拥避 | 20 | 16 | +1 |
| 9 | 拥避 | 21 | 16 | +1 |
| 10 | 拥避 | 22 | 16 | 超时! ssthresh=11, cwnd=1 |
| 11 | 慢开始 | 1 | 11 | 新门限 11 |
| 12 | 慢开始 | 2 | 11 | ×2 |
| 13 | 慢开始 | 4 | 11 | ×2 |
| 14 | 慢开始 | 8 | 11 | ×2 |
| 15 | 慢开始→拥避 | 11 | 11 | 到门限,转拥避 |
| 16 | 拥避 | 12 | 11 | +1 |
| 17 | 拥避 | 13 | 11 | +1 |
关键结论 :cwnd 在 [1, ssthresh] 指数增长,超过后线性增长;超时把它打回 1,3-ACK 只打到 ssthresh。
十二、17 年真题考在哪
数据口径 :以下年份题号来自 zhenti_all.json(2009--2025)计网部分(no 33--40),为单选题;2016、2023 年的综合大题含拥塞控制窗口变化,按公开统计口径列年份。
| 年份(题号) | 考法 | 答案 |
|---|---|---|
| 2009(39) | 拥塞窗口 16KB 超时后,接下来 4 个 RTT 成功传输量 | C |
| 2010(39) | MSS=1000,cwnd=4000,连发两最大段后收确认,rwnd=2000 时发送窗口 | A |
| 2014(38) | 超时 cwnd=8KB,10 个 RTT 后发送窗口(rwnd 恒 10KB) | A |
| 2015(39) | 初始阈值 32KB,MSS=1KB,rwnd=16KB,数据全确认后发送窗口 | A |
| 2017(39) | MSS=1KB,RTT=5ms,rwnd=64KB,发送窗口达 32KB 至少需多久 | A |
| 2020(38) | MSS=1KB,RTT=2ms,cwnd 从 8KB 增到 32KB 最长时间 | D(48ms) |
| 2022(38) | MSS=1KB,cwnd=16KB 超时,再增回 16KB 至少几个 RTT | C(11 RTT) |
| 2023(47) | 综合大题:TCP 连接 + 拥塞控制窗口变化 | 大题 |
| 2025(38) | 图示 seq/ack/rcwnd,慢开始阶段收到确认后还能发几段 | A |
命题规律 :①流量/拥塞控制几乎年年考,小题 + 大题都有;②最爱考数值推演(给 RTT、MSS、初始 cwnd,问若干 RTT 后窗口或时间);③必挖的坑就是「超时 vs 3-ACK 处理不同」「ssthresh 切换点之后不再翻倍」。
十三、易错点清单
| 易错点 | 正确说法 |
|---|---|
| 慢开始增长慢 | 错,是指数增长(每 RTT 翻倍),所有阶段最快 |
| 过了 ssthresh 还在翻倍 | 错,超过门限后改每 RTT +1(线性) |
| 3 个重复 ACK 时 cwnd 置 1 | 错,cwnd = ssthresh(不置 1);只有超时才置 1 |
| 超时与 3-ACK 的 ssthresh 不同 | 错,两者都是 ssthresh = cwnd/2 |
| 报文段「窗口」字段是本方发送窗口 | 错,是本方接收窗口 rwnd |
| 窗口为 0 时彻底不能发 | 错,零窗口探测/确认/紧急数据仍可发 |
| 发送窗口只看 cwnd | 错,取 min(rwnd, cwnd) |
| 2020(38) 用慢开始算最短时间 | 错,问「最长」时间要假设 ssthresh ≤ 起点,全程拥避 +1 |
速查表(文末收藏版)
发送窗口上限 = min(rwnd, cwnd)
rwnd:接收窗口,接收方通告,单位字节,限制「接收方来得及收」
cwnd:拥塞窗口,发送方自估,限制「网络扛得住」
慢开始 cwnd < ssthresh → 每 RTT ×2(指数)
拥塞避免 cwnd > ssthresh → 每 RTT +1(线性)
切换点 cwnd = ssthresh → 转拥避
超时(严重) ssthresh = cwnd/2,cwnd = 1 ← 打回原形
3 个重复 ACK ssthresh = cwnd/2,cwnd = ssthresh ← 只掉到门限(快恢复)
AIMD:加法增大(+1/RTT) + 乘法减小(ssthresh 减半)
零窗口死锁 → 持续计时器 + 1 字节零窗口探测
真题高频:2009(39) 2010(39) 2014(38) 2015(39) 2017(39) 2020(38) 2022(38) 2023(47) 2025(38)
本文依据 :谢希仁《计算机网络(第 8 版)》第 5 章 5.7--5.8;真题年份题号来自 zhenti_all.json(2009--2025 计网 no 33--40);考频口径为公开统计,★ 少不等于不考。
图示:cwnd 随 RTT 的变化曲线------先指数上升(慢开始)到门限 ssthresh,再线性上升(拥塞避免),超时后骤降为 1,如此循环;并标注 ssthresh 切换点