《深入理解 RFC 5681:TCP 拥塞控制》
“网络是共享的,谁也不能太贪婪。”—— TCP 拥塞控制的哲学
朋友们好,今天我们来啃一份互联网世界的“红绿灯规范”——RFC 5681。如果你写过网络程序、调过 TCP 参数、或者只是好奇为什么你在看视频时网络不会突然崩溃,那这篇都值得一读。
一、它是什么?
表格
| 问题 | 答案 |
|---|---|
| RFC 编号 | 5681 |
| 全称 | TCP Congestion Control(TCP 拥塞控制) |
| 状态 | 📘 互联网标准(Standards Track) |
| 发布时间 | 2009 年 9 月(距今已有十余年,但依然核心) |
| 废除了谁 | RFC 2581(又废除了 RFC 2001) |
| 一句话定位 | 给 TCP 装上一个“智能油门”,既不浪费带宽,也不撑爆网络。 |
🔑 核心要解决三个问题
- 新连接怎么开始发数据?—— 答案:慢启动(Slow Start)
- 出现丢包如何应对?—— 答案:快速重传 + 快速恢复(Fast Retransmit/Fast Recovery)
- 空闲太久后怎么恢复?—— 答案:重启窗口(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 有两种主流方式:
- 字节计数法(推荐):累计确认字节数,达到 cwnd 后加 1 SMSS
- 公式法:
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 793 | TCP 基础——序列号、确认号、窗口 |
| RFC 1122 | 主机要求——强制要求拥塞控制实现 |
| RFC 1191 / 4821 | 路径 MTU 发现——决定 SMSS |
| RFC 2988 | 重传定时器——RTO 计算 |
| RFC 3042 | Limited Transmit——前 2 个 dup ACK 时发新数据 |
| RFC 3465 | Appropriate 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 的配合)吗?欢迎留言或私信。
