滑动窗口与流量控制
为什么需要流量控制
发送方和接收方的处理能力往往不匹配:
- 发送方是高性能服务器,每秒能发 1GB
- 接收方是手机,应用层每秒只能处理 10MB
如果没有流量控制,接收方缓存会被迅速填满,后续到达的数据只能丢弃——虽然 TCP 会重传,但造成大量浪费。
流量控制(Flow Control) 解决的是端到端速率不匹配问题:接收方通过窗口通告,告诉发送方"我还能接收多少"。
滑动窗口机制
TCP 窗口以字节为单位。发送方维护一个"发送窗口",只有落在窗口内的数据才能发送:
窗口三要素:
- 已发送已确认:可以移出窗口,释放缓存
- 已发送未确认:等待 ACK,定时器超时需重传
- 允许发送未发送:在窗口范围内,可以立即发送
接收窗口(rwnd)通告
接收方通过 ACK 报文中的 Window 字段通告当前可用接收缓冲区大小:
发送方的实际发送窗口:
有效窗口 = min(rwnd, cwnd)
- rwnd(receiver window):接收方通告的窗口,流量控制
- cwnd(congestion window):拥塞窗口,拥塞控制(见下一章)
发送方取两者较小值,既不超过接收方能力,也不超过网络承受能力。
零窗口与窗口探测
当接收方应用层读取极慢时,Window 字段可能降至 0:
窗口探测(Window Probe):
- 发送方收到零窗口后,启动持续定时器(Persist Timer,通常 5~60 秒)
- 定时器触发时,发送 1 字节数据的窗口探测报文
- 接收方必须回复 ACK(即使 Window 仍为 0)
- 避免双方陷入"死等"(接收方打开窗口的 ACK 丢失时)
窗口缩放选项(Window Scale,RFC 7323)
原始 TCP 首部中 Window 字段仅 16 位,最大 65535 字节。对于高带宽延迟积(BDP)网络,这个窗口严重限制吞吐:
带宽 = 1Gbps = 125MB/s
RTT = 100ms
BDP = 125MB/s × 0.1s = 12.5MB = 12.5 × 1024 × 1024 字节
但最大窗口只有 65535 字节!
理论最大吞吐 = 65535 × 8 / 0.1 = 5.24Mbps(远低于 1Gbps)
窗口缩放选项在三次握手时协商一个 Shift Count(0~14):
实际窗口大小 = Window 字段值 × 2^ShiftCount
例如 Shift Count = 8,Window 字段 = 65535:
实际窗口 = 65535 × 2^8 = 65535 × 256 = 16,776,960 字节 ≈ 16MB
现代操作系统默认启用窗口缩放(Linux net.ipv4.tcp_window_scaling=1)。
糊涂窗口综合征与 Nagle 算法
糊涂窗口综合征(Silly Window Syndrome)
当接收方应用层每次仅读取少量数据(如 1 字节)并立即通告微小窗口时,发送方会发送大量小报文段(Tinygrams),导致网络效率急剧下降。
解决方案:
- 接收端(Clark 算法):通告窗口为 0,直至有足够空间容纳 MSS 或缓冲区半空
- 发送端(Nagle 算法,RFC 896):连接上存在未确认数据时,将小数据缓存,直至收到 ACK 或积累到 MSS 大小再发送
Nagle 算法
if (有未确认的数据在途) {
if (数据长度 >= MSS) {
立即发送
} else {
缓存数据,等待 ACK 到达后再发送
}
} else {
立即发送
}
生活例子:Nagle 像"等公交车"——如果车还没来(无 ACK),你就等更多人一起上车(攒数据);如果车已经到了(有 ACK),你就直接上。
Nagle 的问题:与延迟 ACK 结合时,可能产生"写-等-写-等"的延迟:
- 发送方:Nagle 说"等 ACK"
- 接收方:延迟 ACK 说"等 200ms 或下一段数据"
- 结果:双方互相等待,延迟增加
禁用 Nagle:
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on));
交互式应用(SSH、游戏)通常禁用 Nagle,文件传输应用通常启用。
延迟 ACK(Delayed ACK)
接收方不立即回复 ACK,而是:
- 等待 200ms(或 40ms,取决于实现)
- 或等待下一个数据段到达(捎带 ACK)
目的:减少 ACK 数量,降低网络开销。
问题:与 Nagle 算法结合导致延迟。Linux 可通过 TCP_QUICKACK 禁用:
setsockopt(sockfd, IPPROTO_TCP, TCP_QUICKACK, &on, sizeof(on));
本篇小结
- 流量控制解决发送方与接收方速率不匹配问题
- 滑动窗口:只有窗口内的数据才能发送
- 有效窗口 = min(rwnd, cwnd)
- 零窗口时发送方停止发送,定期发送窗口探测
- 窗口缩放(RFC 7323):Shift Count 0~14,突破 64KB 限制
- 糊涂窗口综合征:接收方 Clark 算法 + 发送方 Nagle 算法
- Nagle + 延迟 ACK 可能互相等待,高实时应用通常禁用两者
动手实践
查看 Linux 窗口缩放和 Nagle 设置:
sysctl net.ipv4.tcp_window_scaling sysctl net.ipv4.tcp_notsent_lowatWireshark 抓包观察窗口变化:
- 查看 ACK 报文的 Window Size 字段
- 观察 "Calculated window size"(已应用缩放因子)
测试 Nagle 的影响:
# 使用 nc 发送小数据,对比开启/关闭 Nagle 的延迟计算:带宽 10Gbps,RTT 50ms,需要多大的窗口才能充分利用带宽?是否需要窗口缩放?