【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,转入拥塞避免。
  • 超时仍然视为严重拥塞,退回慢启动。
相关推荐
BIG_PEI8 小时前
检查并安装Redis
java
大貔貅喝啤酒8 小时前
基于Windows下载安装Android Studio 3.3.2版本教程(2026详细图文版)
android·java·windows·android studio
奋斗的小方8 小时前
Java基础篇09:项目实战
java·开发语言
海兰8 小时前
【第21篇-续】graph-Stream-Node改造为适配openAI模型示例
java·人工智能·spring boot·spring·spring ai
vKd0Ff21L8 小时前
如何在Dev-C++中设置TDM-GCC为默认编译器第九十一篇
java·jvm·c++
武子康9 小时前
Java-221 RocketMQ 消息存储核心原理:CommitLog、ConsumerQueue、IndexFile 与消息过滤机制
java·大数据·分布式·消息队列·rabbitmq·rocketmq·java-rocketmq
北风toto9 小时前
为什么 IntelliJ IDEA Community 无法开发 Vue?——附解决方案
java·vue.js·intellij-idea
programhelp_9 小时前
Google 2026 New Grad SDE VO 三轮面试详解 | 含Behavioral、Coding、Design
java·服务器·数据库
驭渊的小故事9 小时前
java中的进程的详细解析
java·开发语言