【Java EE】TCP—流量控制和拥塞控制

流量控制和拥塞控制

流量控制

什么是流量控制

流量控制是 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 拥塞控制经历了 TahoeReno 两个主要版本,其后发展出 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 时,进入拥塞避免阶段。
  • 每经过 一个 RTTcwnd 增加 1 个 MSS(加法增大)。
  • 使窗口线性增长,避免因突然大幅增加速率而导致网络过载。

丢包的处理

TCP 通过两类事件检测丢包:

  • 超时重传(RTO 超时):表明网络严重拥塞。
  • 收到 3 个重复 ACK:表明后续包已经到达,当前包可能孤立丢失,网络未完全堵塞。

TCP Tahoe(早期版本)

丢包事件 动作
超时 / 3 个重复 ACK ssthresh = cwnd / 2cwnd = 1,重传丢失包,重新进入慢启动
  • 简单粗暴,每次丢包都"一夜回到解放前",恢复效率低。

TCP Reno(改进版本)

丢包事件 动作
3 个重复 ACK ssthresh = cwnd / 2cwnd = ssthresh + 3,进入快恢复
超时 同 Tahoe:ssthresh = cwnd / 2cwnd = 1,重新慢启动
  • Reno 对 3 个重复 ACK 做了关键优化,避免不必要的慢启动。
  • 快恢复 期间:每收到一个重复 ACK,cwnd += 1(模拟发出的包离开网络);当收到新 ACK 时,cwnd = ssthresh,转入拥塞避免。
  • 超时仍然视为严重拥塞,退回慢启动。
相关推荐
神明不懂浪漫几秒前
【第五章】Java中的继承与多态
java·开发语言
AI多Agent协作实战派2 小时前
AI多Agent协作系统实战(十七):凌晨4点,我的AI系统在“假装工作“——3个bug同时爆炸的5小时
java·前端·bug
gaolei_eit2 小时前
Java+Ai+vue
java·spring·maven
qq_2518364572 小时前
基于java Web 动漫视频网站毕业论文
java·开发语言·前端
LayZhangStrive2 小时前
JUC相关的函数、注解、变量杂记
java·面试·多线程·juc
未秃头的程序猿2 小时前
给公司做了个AI客服Agent,用的Spring AI 1.0,3天上线领导拍板了
java·后端·ai编程
曹牧2 小时前
Eclipse 批量文本替换
java·ide·eclipse
程序员清风3 小时前
OpenAI官方发布最新提示词技巧!
java·后端·面试
一只枫林3 小时前
MySQL内、外连接知识点汇总
java·前端·数据库
梅头脑3 小时前
写了3年CRUD,volatile和synchronized的区别还是答不清——直到我画出了这张三性两锁边界图
java