模块二 · 运输层
滑动窗口与流量控制
如果 TCP 每发一个段都要等对方确认,那么不管带宽多大,实际速度都会被"等待时间"锁死。本章讲 TCP 怎么用滑动窗口解决这个问题,以及"流量控制"到底在控制什么。
本章要解决的问题:如果 TCP 每发一个段都要等对方确认,那么不管带宽多大,实际速度都会被"等待时间"锁死。本章讲 TCP 怎么用滑动窗口解决这个问题,以及"流量控制"到底在控制什么。
本章的证据来源:历史实测记录(Get-NetTCPSetting 与 netsh int tcp show global 的窗口相关参数)。机制出自 RFC 9293(TCP)与 RFC 7323(窗口缩放)。
先从一个例子理解概念
如果每发一小份都要等回应,长距离下载会很慢;但一口气全发,又可能让接收端来不及处理。窗口用来管理能同时在路上的量。
未确认数据有多少:在途数据与窗口
已发出但尚未得到相应确认的内容叫在途数据。窗口限制这类内容的数量,常按字节计;它不是屏幕上的窗口。
对方还能收多少:接收窗口与流量控制
接收端通告可接收范围,叫接收窗口(rwnd)。发送端遵守它,以免压垮对方的缓冲,这叫流量控制。缓冲区是暂存待处理数据的内存。
网络能承受多少:拥塞窗口
发送端还会根据网络反馈维护拥塞窗口(cwnd)。rwnd管接收端,cwnd管路径负载;本次新发量还要扣掉已在途数据。
把较大窗口写进有限字段:窗口缩放
TCP窗口字段大小有限,双方可以协商用一个移位因子解释更大范围,叫窗口缩放。这需要双方支持,不是本机开关一开就能单方面生效。
下面再用这些概念看原理、命令和结果。规范与官方资料列在本章末尾,类比只帮助理解,不代替协议规则。
先算一笔账:停等协议有多慢
假设 TCP 是这样工作的:发一个段 → 等确认回来 → 再发下一个。
这叫停等协议,问题立刻就能算出来。
举例:你的网络往返时间(RTT)是 100 毫秒,每个段装 1500 字节。
每发出一段要等一个完整的往返:100 毫秒
所以每秒最多发:1000 ÷ 100 = 10 个段
吞吐量上限:10 × 1500 字节 ≈ 15 KB/s15 KB/s。 也就是说,哪怕你用的是千兆宽带,速度也只有 15 KB/s。
滑动窗口:一次发一批,不用等
解决办法很直接:不要发一个等一个,而是一次发一批。
发送方维护一个窗口(window),表示"我现在可以连续发出多少字节而不用等确认":
发送窗口(比如 4 个段):
[ 段1 ][ 段2 ][ 段3 ][ 段4 ] 还没发
已确认 ↑
可以发的范围发出段 1~4 之后,收到 ACK 确认了段 1、段 2,窗口就向右滑动:
[ 段1 ][ 段2 ] [ 段3 ][ 段4 ][ 段5 ][ 段6 ]
已确认 还在等确认 新进入窗口的
↑
窗口向右滑了关键点:窗口内可以有多少数据"在路上",决定了吞吐能力。
吞吐量 ≈ 窗口大小 ÷ RTT用前面的例子:如果窗口能装 20 个段(30000 字节),RTT 是 100ms:
30000 字节 ÷ 0.1 秒 = 300 KB/s比停等协议的 15 KB/s 快了 20 倍。 窗口越大,能跑满的带宽越高。
接收窗口:这就是"流量控制"
窗口大小不能由发送方随便定。接收方可能会处理不过来 —— 它的内存有限,如果发送方猛发,缓冲区满了,数据只能被丢弃,丢了又要重传,反而更糟。
所以接收方在每一个 ACK 里都告诉发送方:"我还能收多少字节。" 这个字段叫接收窗口(rwnd)。
发送方能发多少 = min( 发送方的拥塞窗口 , 接收方通告的接收窗口 )
↑ 下一章讲 ↑ 本章的主角"流量控制"控制的就是这个接收窗口 —— 目的是不要压垮对方。
窗口字段只有 16 位,这就出了问题
TCP 头里的窗口字段是 16 位,所以它最大只能表示:
2¹⁶ − 1 = 65535 字节(约 64 KB)再算一次账:窗口最多 64 KB,RTT 100 毫秒:
65535 字节 ÷ 0.1 秒 ≈ 655 KB/s655 KB/s。 在上世纪 80 年代这很快,但今天连手机网络都不止这个速度。
问题不在于"窗口只有 64KB",而在于"窗口不够大就填不满带宽延迟积"。 一条 1 Gbps、RTT 100ms 的链路,需要大约 12.5 MB 的在途数据才能跑满 —— 64 KB 差了 200 倍。
解决办法:窗口缩放
RFC 7323(早期是 RFC 1323)定义了一个选项:窗口缩放因子。
双方在SYN/SYN+ACK中交换方向相关的移位因子0–14;协商成功后的非SYN窗口按对应2ⁿ缩放:
通告的窗口值 65535 × 2¹⁴ ≈ 1 GB这样窗口上限从 64 KB 提到了约 1 GB,足够填满今天绝大多数链路。
Get-NetTCPSetting实测结果:
| 参数 | 实测值 | 含义 |
|---|---|---|
AutoTuningLevelLocal | Normal | 接收窗口自动调优(默认档) |
ForceWS | Enabled | 强制启用窗口缩放 |
ScalingHeuristics | Disabled | 不使用启发式的缩放判断 |
| RFC 1323 Timestamps | allowed | 允许使用时间戳选项(和窗口缩放同一批扩展) |
AutoTuningLevelLocal = Normal 是什么意思?
它表示内核会根据实际吞吐情况自动调整接收窗口:发现连接跑得快就调大,发现用不上就调小(省内存)。这是现代系统的常规做法 —— 不需要人工调优,系统自己会找合适的值。
ForceWS是本地策略,不能保证窗口缩放一定协商成功。 RFC7323要求双方在SYN报文交换窗口缩放选项;对端不支持时不能单方面放大。SYN自身窗口字段不缩放。
零窗口:接收方喊停
如果接收方的缓冲区真的满了,它会通告一个窗口大小为 0。
发送方收到之后必须停止发送。但这里有个陷阱:
所以发送方在收到零窗口后不会干等 —— 它会定期发送窗口探测(window probe):一个只带 1 个字节的包,试探对方现在的窗口是多少。
这个细节体现了一个通用原则:协议设计必须考虑到"通知消息本身也可能丢失"。
实测:网卡还有一层合并
除了协议层的窗口,本机的网卡还开着一个优化:
Receive Segment Coalescing State : enabled接收段合并(RSC):硬件或软件实现把连续到达的多个段合并成一个大块再交给协议栈。
好处:减少中断次数和协议栈的处理开销,相当于把"多次小批量处理"变成"一次大批量处理"。这和滑动窗口的思想是一致的 —— 批量处理总是比逐个处理高效。
常见误解
误解一:"窗口大小就是带宽"
不是。窗口大小决定的是"同时在路上"的数据量上限。实际吞吐 ≈ 窗口 ÷ RTT。所以同样的窗口,在低延迟链路上能跑出的吞吐更高。这也是为什么"远距离传输"特别依赖大窗口。
误解二:"窗口字段只有 16 位,所以 TCP 最多只能跑到 655 KB/s"
这是老黄历了。 在RTT=100ms且无缩放的示例条件下约为此值;不是任何RTT下都固定655KB/s,但窗口缩放选项(RFC 1323/7323)把上限提到了约 1 GB。现代系统默认启用(历史实测记录 ForceWS = Enabled)。
误解三:"流量控制和拥塞控制是一回事"
不是。 流量控制解决"接收方处理不过来",拥塞控制解决"网络承受不了"。前者靠接收方通告窗口,后者靠发送方自己估算。在途未确认数据的窗口上限受两者较小值约束;本次还能新发多少要扣除已在途量,并受应用数据和配速等限制。
误解四:"接收方通告了窗口,发送方就可以立刻按这个量发"
不一定。发送方还要受自己的拥塞窗口限制(下一章)。接收方说"我能收 1MB",但如果发送方估算网络只能承受 64KB,那还是只能发 64KB。
本章小结
| 结论 | 类型 |
|---|---|
| 停等协议下吞吐 ≈ 段大小 ÷ RTT,带宽再大也跑不满 | 推理 |
| 带宽延迟积 = 带宽 × 延迟,决定链路能容纳多少在途数据 | 标准 |
| 滑动窗口让发送方连续发送多个段而不必逐个等待确认 | 标准 |
| 吞吐量 ≈ 窗口大小 ÷ RTT | 推理 |
| 接收窗口由接收方在 ACK 中通告,用于流量控制 | 标准 |
| 在途上限受min(cwnd,rwnd)约束,新增可发量还需扣除在途量 | 协议范围 |
| 窗口字段 16 位,上限 64 KB,不足以填满现代高带宽链路 | 标准 |
| 窗口缩放(RFC 7323)把窗口上限提到约 1 GB | 标准 |
实测:本机 AutoTuningLevelLocal = Normal(接收窗口自动调优) | 实测 |
实测:本机 ForceWS = Enabled(强制启用窗口缩放) | 实测 |
| 零窗口时发送方定期发窗口探测,避免双方死锁 | 标准 |
实测:本机 Receive Segment Coalescing = enabled(网卡合并入站段) | 实测 |
自测题
6 题 · 先自己答,再展开对照
网络 RTT 是 50 毫秒,窗口大小是 64 KB。理论吞吐上限大约是多少?
约 1.3 MB/s。 65535 字节 ÷ 0.05 秒 ≈ 1.31 MB/s。注意这个上限和你的实际带宽无关 —— 哪怕你用的是万兆网络,只要窗口只有 64 KB、RTT 是 50ms,吞吐就被锁在 1.3 MB/s。 这就是"带宽延迟积"的约束。如果不做窗口缩放,一条 RTT 200ms 的跨国链路最多能跑多快?为什么这不够用?
约 327 KB/s(65535 ÷ 0.2)。这个速度远远不够用 —— 一条普通的百兆链路理论上有 12.5 MB/s,但窗口限制让实际只能跑到 3% 都不到。跨国链路 RTT 大,需要的窗口更大,所以窗口缩放对长距离传输尤其关键。"流量控制"和"拥塞控制"分别要解决什么问题?它们各自用什么手段?
流量控制解决"接收方处理不过来"(对方内存/处理能力有限,发太猛会导致丢包重传,反而更糟),手段是接收方在 ACK 里通告接收窗口。
拥塞控制解决"网络中间承受不了"(链路带宽有限,发太猛会让路由器队列溢出、丢包加剧),手段是发送方自己估算一个拥塞窗口并动态调整。
在途未确认数据的窗口上限受两者较小值约束;本次还能新发多少要扣除已在途量,并受应用数据和配速等限制。接收方通告窗口为 0,发送方应该怎么做?为什么不能就这么一直等下去?
立刻停止发送,但要定期发送窗口探测(只带 1 字节的报文)去询问对方当前的窗口大小。不能干等着,因为接收方腾出空间后发出的"窗口更新"通知可能丢失 —— 如果双方都在等对方,就会永远僵住(死锁)。定期探测是打破这个僵局的手段。窗口缩放是怎么把上限从 64 KB 提到 1 GB 的?
双方在初始SYN交换选项,分别提供方向相关的缩放因子0–14;非SYN窗口按相应因子缩放,最大65535×2¹⁴字节。本地ForceWS不能代替对端协商。为什么同样大小的窗口,在低延迟链路上能跑出更高的吞吐?
因为吞吐 ≈ 窗口 ÷ RTT。 窗口表示"同时能在路上的数据量",而数据跑完一个往返需要 RTT 的时间。RTT 越小,同样的数据量周转越快,单位时间能送出的就越多。这就是为什么低延迟对网络性能如此重要 —— 它和带宽是两个独立的维度,任何一个不够都会成为瓶颈。
参考资料与适用范围
修订核对日期:2026-10-13。规则按所列协议与实现范围理解;文中的原始命令输出是历史观察,不表示本次重新实测。跨版本、代理/隧道及不同捕获位置需要另行核对。