Appearance
TCP 流量控制
2026 大纲 五(三)4 TCP 流量控制。rwnd 怎么产生与通告、零窗口怎么解死锁、Nagle 与糊涂窗口这两个"小报文段"问题集中在本篇;滑动窗口的边界规则见《TCP 可靠传输》,cwnd 的演变见《TCP 拥塞控制》。
一、定义里没有"网络"两个字
上一篇的窗口是"发送方还能发多少",但那个上限是谁定的?其中一半来自接收方——这就是流量控制。
它的定义只有一句:让发送方的发送速率不要太快,要让接收方来得及接收。
留意这句话里完全没有提到网络。流量控制是接收端控制发送端、点对点、端到端的问题,它完全不关心网络堵不堵。
和拥塞控制怎么分?看瓶颈是某一台机器还是整个网络。 教材摆了一组对照,两个场景的数字都很极端:
| 场景 | 需要什么 | 为什么 |
|---|---|---|
| 某光纤网络链路速率 1000 Gbit/s,一台巨型计算机以 1 Gbit/s 向一台个人电脑传文件 | 需要流量控制,不需要拥塞控制 | 网络带宽足够大,不会拥塞;但巨型计算机必须经常停下来,以便个人电脑来得及接收 |
| 某网络链路速率 1 Mbit/s,1000 台大型计算机接入,其中 500 台分别向另外 500 台以 100 kbit/s 发文件 | 需要拥塞控制 | 问题已不是接收端来不来得及,而是整个网络的输入负载是否超过网络所能承受的 |
两者被弄混还有一个具体原因:某些拥塞控制算法也是向发送端发控制报文、告诉它放慢速率,动作和流量控制一模一样。教材的比喻很好用——水龙头通过管道向水桶放水:水桶太小来不及接是流量控制,管道中有狭窄处、水流被堵塞是拥塞控制。同样是把水龙头拧小,目的完全不一样。
交互可视化
二、rwnd 怎么产生
算出来写进 TCP 首部的窗口字段,通告给发送方。字段 16 位、单位是字节,最大 65535;需要更大窗口要用窗口扩大选项(建连时协商,移位值
rwnd 不是单调递减的,这一条常被想当然。发送方送来数据使它减小,接收方的应用进程读走数据使它增大;而且最大不能超过接收缓存的大小。
由这个式子还能推出一条硬上限:稳态下送进缓存的速率必须等于读走的速率,所以
与链路带宽无关。应用完全不读时吞吐量就是 0——流量控制不是"慢一点",而是"接收方不消费就彻底不动"。
配合上一篇那个公式一起看:
三、零窗口死锁,与打破它的那 1 个字节
死锁是这样形成的:B 通告 rwnd=0,A 停发;B 后来腾出了空间,发出新的窗口通告,但这个通告丢了。
为什么它不会自愈,才是这一节的要点:窗口通告是捎带在 ACK 里的,而 ACK 是不需要被确认的——B 发出去就不管了,它永远不会知道这个通告丢了,自然也不会重发;而 A 又不敢发数据。双方互相等待,没有任何一方有理由主动打破僵局。
解法在发送方侧:持续计时器。 A 收到零窗口通知就启动它,到期发出一个零窗口探测报文段(仅携带 1 字节数据),对方在确认这个探测报文段时会给出现在的窗口值。窗口仍是零 → 重新设置持续计时器继续等;窗口不是零 → 僵局打破。 探测报文段自己丢了也不怕,计时器会再次超时重发。
"零窗口"不等于"什么都不收",这处措辞要说准。TCP 规定即使设置为零窗口,也必须接收零窗口探测报文段、确认报文段和携带紧急数据的报文段——它们都不是"要塞进接收缓存的普通数据"。准确表述是不再接收普通数据,而不是不再接收任何报文段。
顺带说明一句:零窗口通告不是错误,它是正常的流量控制行为,说明接收方缓存暂时满了,发送方不会因此断开连接。
零窗口从出现到被打破的完整逐步走查(想看清死锁怎么形成、持续计时器怎么解开时展开)
A 向 B 发送数据。B 的接收缓存为 600 字节,连接建立时通告 rwnd=600。A 的初始序号为 1,每个报文段携带 200 字节。事件序列:① A 连发三个报文段;② B 的应用进程读走 300 字节、随即通告新窗口,但这个通告报文段在网络中丢失;③ 之后 A 恢复发送。
第 1 步:三个报文段依次到达。
| 步 | 报文段 | B 缓存已用 | B 通告 rwnd | B 通告 ack |
|---|---|---|---|---|
| ① | A→B seq=1,200 B | 200 | 201 | |
| ② | A→B seq=201,200 B | 400 | 401 | |
| ③ | A→B seq=401,200 B | 600 | 601 |
应用进程一直没读,缓存就一路涨:rwnd 减少的唯一原因就是"收到了数据但没人读走"。
第 2 步:零窗口出现。 A 收到 ack=601, rwnd=0,必须停止发送数据。
第 3 步:接收方腾出空间,但好消息丢了。 应用进程读走 300 字节,缓存占用降到 300,B 通告 ack=601, rwnd=300——这个报文段在网络中丢失。此时正是教材描述的死锁:
A 一直等待收到 B 发送的非零窗口的通知,而 B 也一直等待 A 发送的数据。如果没有其他措施,这种互相等待的死锁局面将一直延续下去。
第 4 步:持续计时器打破僵局。 A 在收到零窗口通知时就启动了持续计时器。计时器到期,A 发出一个零窗口探测报文段(仅携带 1 字节数据,seq=601)。
第 5 步:探测得到回应。 B 此刻有 300 字节空闲,收下这 1 字节:
| 缓存已用 | 通告 rwnd | 通告 ack |
|---|---|---|
| 602 |
第 6 步:A 恢复发送。
| 报文段 | B 缓存已用 | B 通告 rwnd | B 通告 ack |
|---|---|---|---|
A→B seq=602,200 B | 501 | 802 |
第 7 步:另一条分支。 如果第 5 步时 B 的窗口仍然是 0(应用还是没读),B 就在确认里照实回 rwnd=0,A 重新设置持续计时器继续等——"如果窗口仍然是零,那么收到这个报文段的一方就重新设置持续计时器。如果窗口不是零,那么死锁的僵局就可以打破了。"
四、两个"小报文段"问题:成因相反,思路相同
先看量级有多糟。 用户仅发 1 个字符,加上 20 字节 TCP 首部得 21 字节报文段,再加 20 字节 IP 首部得 41 字节的 IP 数据报——而线路上实际要传总长度 162 字节共 4 个报文段(数据、确认、回显、对回显的确认)。载荷率:1 字节 → 2.44%,300 字节 → 88.24%,1460 字节 → 97.33%。
这两个问题的成因正好相反,判据是"谁制造了小报文段"。
发送方制造的:应用一次只交 1 个字节。 解法是 Nagle 算法——① 把第一个数据字节先发送出去,后面到达的字节都缓存起来;② 收到对第一个数据字节的确认后,把发送缓存中的所有数据组装成一个报文段发出;③ 另规定当到达的数据已达发送窗口大小的一半、或已达报文段的最大长度(MSS)时,就立即发送一个报文段。
第 2 条能自动适配网络快慢,这是 Nagle 最漂亮的地方。 它没有设任何定时器,而是用"上一个确认什么时候回来"作节拍器:网络快 → 确认回得快 → 缓存时间短、报文段小、时延低;网络慢 → 确认回得慢 → 攒的数据多、报文段大、效率高。它把"该攒多久"这个决定交给了网络自己。
第 3 条是为了打开吞吐上限。 只靠第 2 条,一次只能有一个未确认的报文段在路上,长肥管道上吞吐会被卡死;加上"攒够半个窗口或一个 MSS 就立刻发",这个上限才被打开。两条缺一不可。
接收方制造的:糊涂窗口综合征。 接收缓存已满而应用一次只读 1 字节,接收方就立刻通告 rwnd=1,发送方立刻发 1 字节过来,往复不止。解法是 Clark 方案——让接收方等到接收缓存已有足够空间容纳一个最长报文段、或已有一半空闲的空间才通告,在此之前继续通告 0。
两种方法可以配合使用,共同的思路是同一句话:宁可等一等,也不要发(或通告)一个几乎不含信息的东西。
本节小结
- 流量控制是接收端控制发送端的端到端问题,与网络堵不堵无关;判据是瓶颈在"某一台机器"还是"整个网络"。
接收缓存大小 − 已接收但未被读走的数据量,它不单调递减(应用读走会让它变大),且最大不超过接收缓存。 - 零窗口死锁的根源是"ACK 不需要被确认":窗口通告捎带在 ACK 里丢了,B 不知道、不会重发,A 不敢发——双方互相等待。解法是发送方的持续计时器 + 零窗口探测报文段(仅 1 字节);且零窗口时仍必须接收零窗口探测、确认报文段和携带紧急数据的报文段。
- 两个"小报文段"问题成因相反、思路相同:发送方侧用 Nagle(先发一个字节、收到确认再把缓存里的一起发,另加"攒够半个窗口或一个 MSS 立即发"打开吞吐上限),接收方侧用 Clark 方案(等缓存能装下一个 MSS 或空出一半才通告),两者可配合使用。发送窗口取
,长期吞吐量则由应用的读取速率封顶。
考点速记
流量控制在真题里被考过的形式只有一种:在某个时刻,甲还能再发多少。 而这一种的两道题,恰好各卡住
① rwnd 一通告就成了瓶颈(cn-2010-39)。MSS = 1000 B,甲当前 cwnd = 4000 B,连续发出两个最大段(2000 B 在飞),随后收到第一个段的确认,其中通告 rwnd = 2000 B,问甲此时还能再发多少。
三步走:
- 发送窗口
B —— rwnd 成了瓶颈; - 已发送未确认 = 第 2 个段的 1000 B(第 1 个刚被确认,已经出窗口了);
- 还能发
B。
答 A。四个选项 1000/2000/3000/4000 精确对应四种漏算:答 2000 的忘了扣已发未确认;答 4000 的直接用了 cwnd、没取 min;答 3000 的用 cwnd 扣了 1000。"取 min"和"扣掉已发未确认"这两步一步都不能省。
② 接收缓存被填满,rwnd 一路被榨干(cn-2015-39)。这道题设计得很巧:ssthresh = 32 KB、MSS = 1 KB、乙的接收缓存 16 KB,且乙收到的数据全部存入缓存、不被取走。问 4 个 RTT 后甲的发送窗口。
要点是两条曲线一升一降,同时跟踪。 cwnd 在慢开始阶段每轮翻倍;而 rwnd 每轮都被吃掉相应的量、且永远不会回升(应用不读):
| 轮次 | 本轮 cwnd | 本轮实发 | 乙缓存已用 | 轮末 rwnd |
|---|---|---|---|---|
| 第 1 个 RTT | 1 KB | 1 KB | 1 | 15 |
| 第 2 个 RTT | 2 KB | 2 KB | 3 | 13 |
| 第 3 个 RTT | 4 KB | 4 KB | 7 | 9 |
| 第 4 个 RTT | 8 KB | 8 KB | 15 | 1 |
4 个 RTT 之后 cwnd 已涨到 16 KB,可 rwnd 只剩 1 KB → 发送窗口
选项里的 32 KB 是 ssthresh、16 KB 是 cwnd 或接收缓存——只盯着 cwnd 算的人一定会落进去。这道题的全部价值就是那句"乙收到的数据全部存入缓存,不被取走":它把 rwnd 变成了一个单调递减到底的量,流量控制彻底压过了拥塞控制。
本篇练习区里还会出现两道题,它们同样卡在
本篇的零窗口死锁、持续计时器、Nagle 与 Clark 在真题里不单独成题,但它们各自回答了一个"为什么":零窗口那条解释了四种计时器里持续计时器为什么必须存在于发送方;Nagle 那条则是 TCP 首部开销 1/41 那个数字的直接对策。
易错:发送窗口 =
,取完 min 还要再扣掉已发送未确认的部分。 两步都不能省。
易错:rwnd 不是单调递减的——应用读走数据会让它变大;但题面说"不取走"时它就一路降到底。
易错:收到某个段的确认,说明该段已出窗口,不再计入"已发送未确认"。
易错:流量控制与拥塞控制的动作相同、目的不同,判据是瓶颈在某一台机器还是整个网络。
易错:零窗口不等于什么都不收——零窗口探测、确认报文段、携带紧急数据的报文段仍必须接收。
易错:零窗口死锁的根源是"ACK 不需要被确认",所以丢了的窗口通告不会被重发。
易错:Nagle 在发送方、Clark 在接收方。 判据是谁制造了小报文段。
易错:长期吞吐量由应用读取速率封顶,与链路带宽无关。
教材出处
- 谢希仁《计算机网络》(第 8 版)p236(5.7.1 利用滑动窗口实现流量控制):定义原文——"所谓流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收";rwnd 的通告"发送方的发送窗口不能超过接收方给出的接收窗口的数值。请注意,TCP 的窗口单位是字节,不是报文段";以及零窗口死锁的完整场景——"B 向 A 发送了零窗口的报文段后不久,B 的接收缓存又有了一些存储空间。于是 B 向 A 发送了 rwnd=400 的报文段。然而这个报文段在传送过程中丢失了。A 一直等待收到 B 发送的非零窗口的通知,而 B 也一直等待 A 发送的数据。如果没有其他措施,这种互相等待的死锁局面将一直延续下去"。
- 同书 p237:持续计时器的完整规则——"TCP 为每一个连接设有一个持续计时器。只要 TCP 连接的一方收到对方的零窗口通知,就启动持续计时器。若持续计时器设置的时间到期,就发送一个零窗口探测报文段(仅携带 1 字节的数据),而对方就在确认这个探测报文段时给出了现在的窗口值。如果窗口仍然是零,那么收到这个报文段的一方就重新设置持续计时器。如果窗口不是零,那么死锁的僵局就可以打破了"。同页脚注给出零窗口时仍必须接收的三类报文段:"TCP 规定,即使设置为零窗口,也必须接收以下几种报文段:零窗口探测报文段、确认报文段和携带紧急数据的报文段。"
- 同书 p237(5.7.2 TCP 的传输效率):TELNET 的量级计算——"假设用户只发 1 个字符,加上 20 字节的首部后,得到 21 字节长的 TCP 报文段。再加上 20 字节的 IP 首部,形成 41 字节长的 IP 数据报……这样,用户仅发 1 个字符时,线路上就需传送总长度为 162 字节共 4 个报文段";Nagle 算法原文——"发送方就把第一个数据字节先发送出去,把后面到达的数据字节都缓存起来。当发送方收到对第一个数据字节的确认后,再把发送缓存中的所有数据组装成一个报文段发送出去,同时继续对随后到达的数据进行缓存……Nagle 算法还规定,当到达的数据已达到发送窗口大小的一半或已达到报文段的最大长度时,就立即发送一个报文段";糊涂窗口综合征的场景与 Clark 方案的门限——"可以让接收方等待一段时间,使得或者接收缓存已有足够空间容纳一个最长的报文段,或者等到接收缓存已有一半空闲的空间"。
- 同书 p238:两种方法配合使用的说明——"上述两种方法可配合使用。使得在发送方不发送很小的报文段的同时,接收方也不要在缓存刚刚有了一点小的空间就急忙把这个很小的窗口大小信息通知给发送方"。
- 同书 p238–p239(5.8.1 拥塞控制的一般原理):流量控制与拥塞控制的区分——"流量控制往往是指点对点通信量的控制,是个端到端的问题(接收端控制发送端)";两个对照场景(1000 Gbit/s 光纤网上巨型计算机向个人电脑传文件"显然,网络本身的带宽是足够大的,因而不存在产生拥塞的问题。但流量控制却是必需的";1 Mbit/s 网上 500 台向 500 台发文件"现在的问题已不是接收端的大型计算机是否来得及接收,而是整个网络的输入负载是否超过网络所能承受的");以及两者常被弄混的原因和图 5-22 的水龙头比喻。
- 同书 p245:式 (5-9) 与判读方法——"发送方窗口的上限值 = Min[rwnd, cwnd]……当 rwnd < cwnd 时,是接收方的接收能力限制发送方窗口的最大值。反之,当 cwnd < rwnd 时,则是网络的拥塞程度限制发送方窗口的最大值"。
- 同书 p232:接收缓存与接收窗口的关系——"如果接收应用程序来不及读取收到的数据,接收缓存最终就会被填满,使接收窗口减小到零。反之,如果接收应用程序能够及时从接收缓存中读取收到的数据,接收窗口就可以增大,但最大不能超过接收缓存的大小"。