《深入理解 RFC 5681:TCP 拥塞控制》

📅 2026/7/6 21:30:25 👁️ 阅读次数 📝 编程学习
《深入理解 RFC 5681:TCP 拥塞控制》

“网络是共享的,谁也不能太贪婪。”—— TCP 拥塞控制的哲学

朋友们好,今天我们来啃一份互联网世界的“红绿灯规范”——RFC 5681。如果你写过网络程序、调过 TCP 参数、或者只是好奇为什么你在看视频时网络不会突然崩溃,那这篇都值得一读。


一、它是什么?

表格

问题答案
RFC 编号5681
全称TCP Congestion Control(TCP 拥塞控制)
状态📘 互联网标准(Standards Track)
发布时间2009 年 9 月(距今已有十余年,但依然核心)
废除了谁RFC 2581(又废除了 RFC 2001)
一句话定位给 TCP 装上一个“智能油门”,既不浪费带宽,也不撑爆网络。

🔑 核心要解决三个问题

  1. 新连接怎么开始发数据?—— 答案:慢启动(Slow Start)
  2. 出现丢包如何应对?—— 答案:快速重传 + 快速恢复(Fast Retransmit/Fast Recovery)
  3. 空闲太久后怎么恢复?—— 答案:重启窗口(Restart Window)

二、核心机制:四个算法,一张流程图

🚦 发送端的生命周期

+------------------+ | 连接建立完成 | | cwnd = IW | +--------+---------+ | cwnd < ssthresh? ┌──── YES ──┴── NO ─────┐ ↓ ↓ +------------------+ +------------------+ | 慢启动 | | 拥塞避免 | | 每 RTT 翻倍 cwnd | | 每 RTT +1 SMSS | +--------+---------+ +--------+---------+ | | +------ 丢包发生? ---------+ | +--------+---------+ | 检测到丢包方式 | +----+----+----+----+ | | | ┌────+ + +────┐ ↓ ↓ ↓ 3个重复ACK 超时 SACK/ECN | | +---------+ +---------+ | 快速重传&恢复 | 重置 cwnd=1 | cwnd 减半 | 重新慢启动 +------------------+

🧊 生活类比:高速公路的智能限速

想象你在一条高速上开车,cwnd(拥塞窗口)是你能同时在路上跑的“车数”的许可证。

  • 慢启动:刚上高速,每次前车到达目的地(收到 ACK),你就多放两辆车出去——指数级放行,快速试探道路容量。
  • 拥塞避免:当发现路上车已经不少了(cwnd 超过阈值),改为每次前车到了才多放一辆——线性增长,保守试探。
  • 丢包 = 车祸:收到 3 个重复 ACK 相当于“后面连续来的车都报告前面有事故”——立即降速到一半,并补发丢失的车。
  • 超时 = 彻底堵死:很久没收到任何反馈,只能停车(cwnd=1),从头探路。

三、三个关键窗口和两个阈值

cwnd(拥塞窗口)

TCP 发送端的状态变量,限制“已发送但未确认”的数据量。升升降降全看网络脸色。

ssthresh(慢启动阈值)

  • 初始时设为极大值(通常是最大通告窗口)
  • 丢包时设为max(FlightSize / 2, 2 × SMSS)
  • 决定使用慢启动还是拥塞避免的分水岭

FlightSize(在途数据)

“已发出但未收到累计确认”的数据量。注意:很多实现错误地用了 cwnd 来计算 ssthresh,这是不对的,应该用 FlightSize。


四、开发者必看:5 个最容易踩的坑

🕳️ 坑 1:初始窗口不是固定的“3 段”

很多人背了“初始窗口是 3 段”的口诀,但标准明确分三档:

SMSS > 2190 → IW = 2 × SMSS 1095 < SMSS ≤ 2190 → IW = 3 × SMSS SMSS ≤ 1095 → IW = 4 × SMSS

如果你用了巨型帧(SMSS 很大),初始窗口可能是2 段而不是 3 段!

🕳️ 坑 2:超时后 ssthresh 不是每次都减半

第一次超时:

ssthresh = max(FlightSize / 2, 2 * SMSS) cwnd = 1 * SMSS

但如果是同一段数据第二次超时

  • ssthresh 保持原值,不再减半
  • 只有 cwnd 重置到 1

实现时要注意区分“同一段的重传”和“新段的超时”。

🕳️ 坑 3:拥塞避免阶段的 ACK 计数要小心

拥塞避免期增加 cwnd 有两种主流方式:

  1. 字节计数法(推荐):累计确认字节数,达到 cwnd 后加 1 SMSS
  2. 公式法cwnd += SMSS * SMSS / cwnd

注意:整数运算中,当cwnd > SMSS * SMSS时公式可能返回 0,标准要求round up 到 1 字节

🕳️ 坑 4:延迟确认的两个“紧箍咒”

  • 每两个全尺寸段必须至少发一个 ACK(SHOULD)
  • 等待 ACK 不得超过 500ms(MUST)

如果接收方 MSS 很大但发送方实际段很小,会导致“延展 ACK”问题——性能可能严重退化。

🕳️ 坑 5:空闲重启的误区

TCP 闲置超过一个 RTO 后,发送方的 cwnd 必须“缩水”到:

RW = min(IW, cwnd)

但标准特别提醒:应该以“上次发送数据”为准,而不是“上次接收数据”。否则持久 HTTP 连接中,服务器收到请求后可能误以为连接“不是空闲”,用过大窗口发送,造成突发拥塞。


五、协议间的配合关系

RFC 5681 不是孤立存在的,它依赖于一大套 RFC 家族:

表格

相关 RFC作用
RFC 793TCP 基础——序列号、确认号、窗口
RFC 1122主机要求——强制要求拥塞控制实现
RFC 1191 / 4821路径 MTU 发现——决定 SMSS
RFC 2988重传定时器——RTO 计算
RFC 3042Limited Transmit——前 2 个 dup ACK 时发新数据
RFC 3465Appropriate Byte Counting——增强 cwnd 增长鲁棒性
RFC 3390增大初始窗口——本标准的 IW 依据
RFC 3517 / 3782高级丢包恢复算法(SACK-based / NewReno)

六、安全考虑:破坏者能做什么?

如果用一句话回答:RFC 5681 设计的拥塞控制对整个互联网的稳定性至关重要。

攻击者可以通过伪造重复 ACK让发送方膨胀 cwnd,或通过ACK 分割攻击(ACK Division)诱导发送方过快增长窗口。

标准给出的防御手段:使用 Appropriate Byte Counting(RFC 3465),即按字节计数而非按段计数来增加窗口,这样做可以在一定程度上抵御恶意接收者通过“分拆确认”来欺骗发送方。


🎯 一句话记住这个协议

RFC 5681 就是 TCP 的“自动驾驶系统”——用慢启动试探路况、用拥塞避免保持稳定、用快速重传及时纠错、用快速恢复避免过度刹车。


本文基于 RFC 5681(September 2009)撰写,已考虑最新勘误与配套 RFC。你需要进一步了解某个算法的细节(如 NewReno vs SACK 的差异、或 ECN 的配合)吗?欢迎留言或私信。