流量控制和拥塞控制
- 流量控制
-
- 什么是流量控制
- [每个 Socket 独享一对缓冲区](#每个 Socket 独享一对缓冲区)
- [窗口扩大因子(Window Scale)](#窗口扩大因子(Window Scale))
- 零窗口与坚持定时器(窗口探测)
-
- [零窗口(Zero Window)](#零窗口(Zero Window))
- [坚持定时器与窗口探测(Persist Timer & Window Probe)](#坚持定时器与窗口探测(Persist Timer & Window Probe))
- 拥塞控制
-
- [流量控制 vs 拥塞控制](#流量控制 vs 拥塞控制)
- [拥塞控制的核心阶段与算法(Tahoe / Reno)](#拥塞控制的核心阶段与算法(Tahoe / Reno))
-
- 关键变量
- 慢启动(指数增长)
- 拥塞避免(加法增大)
- 丢包的处理
- [TCP Tahoe(早期版本)](#TCP Tahoe(早期版本))
- [TCP Reno(改进版本)](#TCP Reno(改进版本))
流量控制
什么是流量控制
流量控制是 TCP 的核心机制之一,完全由接收方驱动 ------接收方根据自己的接收缓冲区剩余空间,实时告知发送方"还能发多少字节",从而防止发送方过快的发送导致接收方溢出。该信息通过 TCP 报头中的 16 位窗口大小 字段携带。
| 字段 | 大小 | 说明 |
|---|---|---|
| 窗口大小 | 2 字节 | 接收方当前可用的接收缓冲区大小(单位:字节),也称为 通告窗口(rwnd) |
发送方会维护一个 发送窗口,确保"已发送但未确认"的数据总量始终 ≤ 接收方通告的窗口。每收到一个 ACK,发送方就根据其中的窗口字段动态调整自己还能发送的数据量。
接收方 发送方 接收方 发送方 应用程序读取变慢,缓冲区剩余变小 发送窗口缩小为2000 发送数据 (SEQ:1~1000) ACK=1001, 窗口=3000 发送数据 (SEQ:1001~4000) ACK=4001, 窗口=2000
每个 Socket 独享一对缓冲区
每个 TCP 连接在内核中都分配了独立的 接收缓冲区 和 发送缓冲区,流量控制作用在单个连接上,彼此隔离。
- 接收缓冲区:暂存已到达但应用程序尚未读取的数据。
- 发送缓冲区:暂存应用程序要发送、但网络尚未发送或尚未确认的数据。
内核空间
用户空间
read/write
数据流入
数据流出
应用程序
Socket 内核对象
接收缓冲区
Recv-Q
发送缓冲区
Send-Q
TCP 协议栈
IP 层
- 一条连接的通告窗口大小,就是该连接 接收缓冲区剩余空闲空间。
- 即使同一台机器上有数百个连接,每个连接的窗口独立计算,互不干扰。
窗口扩大因子(Window Scale)
16 位窗口字段最大只能表示 65535 字节(64 KB),在高速高延迟网络(如数据中心、洲际光纤)中会成为严重瓶颈。
为此,TCP 在握手阶段通过 窗口扩大因子 选项协商一个倍数:
Server Client Server Client 连接建立后,实际窗口 = 窗口字段 << 扩大因子 SYN, 选项:窗口扩大因子=6 SYN+ACK, 选项:窗口扩大因子=6
计算公式 :实际窗口大小 = 窗口字段值 × 2^扩大因子
左移 6 位
报头窗口字段: 1024
实际窗口大小: 65536 字节
- 扩大因子可在 0~14 之间,实际窗口最高可达 1 GB(65535 × 2^14)。
- 该选项仅在 SYN 包中协商,连接建立后不可更改,因此需要双方都支持。
零窗口与坚持定时器(窗口探测)
零窗口(Zero Window)
当接收方应用程序读取数据太慢,接收缓冲区被完全填满时,接收方会通告 窗口 = 0 。
发送方收到后必须立即停止发送数据,进入 零窗口状态。
坚持定时器与窗口探测(Persist Timer & Window Probe)
死锁风险 :假设发送方因窗口为 0 而停发,随后接收方应用读取了数据,缓冲区腾出空间,并发送了一个携带新窗口的 ACK。如果这个 ACK 恰好丢失,发送方将永远等待窗口更新,接收方则等待数据,形成死锁。
为解决这一问题,TCP 设计了 坚持定时器 与 窗口探测 机制:
- 发送方进入零窗口状态时,启动 坚持定时器。
- 定时器超时后,发送方构造一个 窗口探测报文(通常包含 1 字节数据,SEQ 为对方期望的下一个序号)。
- 接收方收到探测报文后,必须回复 ACK,即使缓冲区仍满也会将当前窗口大小放入 ACK。
- 若窗口仍为 0,发送方重置定时器,继续等待并探测(探测间隔逐渐增大);一旦窗口 > 0,立即恢复发送。
接收方(缓冲区满) 发送方 接收方(缓冲区满) 发送方 进入零窗口状态,启动坚持定时器 定时器超时后再次探测 break [窗口打开] alt [缓冲区仍满] [缓冲区释放] loop [坚持探测,直到窗口 > 0] 窗口恢复,重新开始发送 ACK=4001, 窗口=0 窗口探测 (SEQ=4001, 1 字节) ACK=4001, 窗口=0 ACK=4001, 窗口=2000 取消坚持定时器 发送数据 (SEQ=4001 起)
- 探测报文自身也是可靠的------如果探测包丢失,TCP 会重传,确保最终"敲"出新的窗口值。
- 坚持定时器的超时时间通常采用指数退避策略(如 1s、2s、4s ... 最长 60s),避免频繁探测浪费资源。
通过这一机制,即使在 ACK 丢包的最坏情况下,连接也不会永久死锁,保证了流量控制的完整性。
拥塞控制
TCP 为保证可靠性,必须同时解决两个问题:接收方是否来得及处理 ,以及网络链路是否足够通畅 。前者由流量控制 负责,后者由拥塞控制负责。两者协同工作,最终决定了发送方的实际发送速率。
流量控制 vs 拥塞控制
| 机制 | 依据 | 目标 | 限制对象 |
|---|---|---|---|
| 流量控制 | 接收方的处理能力(接收窗口 rwnd) |
防止发送方淹没接收方 | 发送窗口 = min(rwnd, cwnd) |
| 拥塞控制 | 传输链路的转发能力(拥塞窗口 cwnd) |
防止网络过载导致丢包 | 同上 |
- rwnd:接收方在 TCP 首部中通告的窗口大小,反映其缓冲区剩余空间。
- cwnd:发送方自行维护的拥塞窗口,反映对网络拥塞程度的估计。
最终发送窗口是两者取小
例如,即使网络很通畅(cwnd 很大),若接收端处理极慢(rwnd 很小),发送方也只能降低速率,避免接收方缓冲区溢出。
发送方
min (rwnd, cwnd)
实际发送窗口
接收方通告 rwnd
拥塞控制计算 cwnd
接收方太慢,流量控制管;网络太堵,拥塞控制管。
拥塞控制的核心阶段与算法(Tahoe / Reno)
经典的 TCP 拥塞控制经历了 Tahoe 和 Reno 两个主要版本,其后发展出 NewReno、CUBIC 等,但核心思想均来自这两个基础版本。它们共同遵循 AIMD(加法增大/乘法减小) 原则:无丢包时缓慢增大窗口,探测可用带宽;发生丢包时迅速缩小窗口,减轻网络负担。
主要阶段包括:
- 慢启动 (Slow Start)
- 拥塞避免 (Congestion Avoidance)
- 快重传 (Fast Retransmit)
- 快恢复 (Fast Recovery) ------ Reno 引入
TCP Reno 拥塞窗口变化示例(ssthresh=16) 0 2 4 6 8 10 12 14 16 18 20 传输轮次 30 28 26 24 22 20 18 16 14 12 10 8 6 4 2 0 cwnd (MSS)
慢启动(指数增长) 轮次 0-4 cwnd=1→2→4→8→16 第4轮 cwnd 达到 ssthresh,转入拥塞避免 拥塞避免(线性增长) 轮次 5-11 cwnd=17→18→...→24 加法增大,每RTT +1 MSS 快重传 & 快恢复 轮次 11末 收到 3 个重复 ACK, 触发快重传 ssthresh=cwnd/2=12, cwnd=12+3=15 轮次 12 每收重复 ACK 则 cwnd+1,cwnd=16 轮次 13 收到新 ACK ,cwnd=ssthresh=12, 转入拥塞避免 拥塞避免(恢复后) 轮次 14+ cwnd=13→14→... 继续线性增长 TCP Reno 拥塞窗口变化阶段 (ssthresh=16)
关键变量
cwnd:拥塞窗口,动态变化,控制发送速率。ssthresh:慢启动阈值,cwnd 达到该值后从慢启动切换为拥塞避免。
慢启动(指数增长)
- 初始
cwnd很小(通常为 1 ~ 10 个 MSS)。 - 每收到 一个 ACK,
cwnd增加 1 个 MSS。
由于一个 RTT 内会有多个 ACK 返回,实际效果是 每经过一个 RTT,cwnd 翻倍(指数增长)。 - 目的:快速探测网络可用带宽,尽快到达合理的发送速率。
称"慢启动"是因它从极小窗口开始,但其指数增长曲线实际上很快。
拥塞避免(加法增大)
- 当
cwnd >= ssthresh时,进入拥塞避免阶段。 - 每经过 一个 RTT ,
cwnd增加 1 个 MSS(加法增大)。 - 使窗口线性增长,避免因突然大幅增加速率而导致网络过载。
丢包的处理
TCP 通过两类事件检测丢包:
- 超时重传(RTO 超时):表明网络严重拥塞。
- 收到 3 个重复 ACK:表明后续包已经到达,当前包可能孤立丢失,网络未完全堵塞。
TCP Tahoe(早期版本)
| 丢包事件 | 动作 |
|---|---|
| 超时 / 3 个重复 ACK | ssthresh = cwnd / 2,cwnd = 1,重传丢失包,重新进入慢启动 |
- 简单粗暴,每次丢包都"一夜回到解放前",恢复效率低。
TCP Reno(改进版本)
| 丢包事件 | 动作 |
|---|---|
| 3 个重复 ACK | ssthresh = cwnd / 2,cwnd = ssthresh + 3,进入快恢复 |
| 超时 | 同 Tahoe:ssthresh = cwnd / 2,cwnd = 1,重新慢启动 |
- Reno 对 3 个重复 ACK 做了关键优化,避免不必要的慢启动。
- 快恢复 期间:每收到一个重复 ACK,
cwnd += 1(模拟发出的包离开网络);当收到新 ACK 时,cwnd = ssthresh,转入拥塞避免。 - 超时仍然视为严重拥塞,退回慢启动。