飞翔飞翔
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
  • 数据库

    • SQL教程
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON教程
  • 工具

    • Markdown指南
  • Git

    • GitFlow
  • Quartz

    • Quartz教程
  • Java

    • Java设计模式
  • 缓存

    • Redis教程
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
  • 数据库

    • SQL教程
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON教程
  • 工具

    • Markdown指南
  • Git

    • GitFlow
  • Quartz

    • Quartz教程
  • Java

    • Java设计模式
  • 缓存

    • Redis教程
联系
阿里云
  • 学习路径
  • IP协议

    • 认识 IP 协议与网络层定位
    • IPv4 编址体系与分类地址
    • CIDR 与子网划分实战
    • IPv6 编址体系
    • ARP 协议详解
    • NDP 协议详解
    • IGMP 与 MLD 组播侦听发现
    • VRRP 与网关冗余
    • IPv4 数据报首部解析
    • IPv6 数据报与扩展首部
    • IPsec 安全扩展
    • 分片、MTU 与路径发现
    • ICMP 与 Traceroute 原理
    • 路由基础与转发流水线
    • 动态路由协议
    • NAT 网络地址转换
    • DHCP 与自动配置
    • Wireshark 与命令行工具
    • IPv6 过渡技术
    • IP 协议栈排障与攻击防御
  • TCP协议

    • 认识 TCP 协议与传输层定位
    • TCP 报文段首部解析
    • 三次握手与连接建立
    • 四次挥手与连接释放
    • TCP 有限状态机
    • 序列号与确认机制
    • 超时重传与 RTO 计算
    • 滑动窗口与流量控制
    • 拥塞控制基础
    • 现代拥塞控制算法
    • TCP 选项与扩展
    • TCP 性能调优与内核参数
    • Nagle 算法与糊涂窗口综合征
    • TCP 定时器与 Keep-Alive 机制
    • TCP 安全与攻击防御
    • TCP 与上层/下层交互
    • TCP 综合实践与排障

滑动窗口与流量控制

为什么需要流量控制

发送方和接收方的处理能力往往不匹配:

  • 发送方是高性能服务器,每秒能发 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 可能互相等待,高实时应用通常禁用两者

动手实践

  1. 查看 Linux 窗口缩放和 Nagle 设置:

    sysctl net.ipv4.tcp_window_scaling
    sysctl net.ipv4.tcp_notsent_lowat
    
  2. Wireshark 抓包观察窗口变化:

    • 查看 ACK 报文的 Window Size 字段
    • 观察 "Calculated window size"(已应用缩放因子)
  3. 测试 Nagle 的影响:

    # 使用 nc 发送小数据,对比开启/关闭 Nagle 的延迟
    
  4. 计算:带宽 10Gbps,RTT 50ms,需要多大的窗口才能充分利用带宽?是否需要窗口缩放?

上一页
超时重传与 RTO 计算
下一页
拥塞控制基础