专栏: 《计算机网络基础》· 第三篇·传输层
承接: 《TCP 专章:三次握手之后,可靠管道里发生了什么?》
讲解维度: 概念 → 原理 → 应用 → 问题定位
读完你能: 分清 rwnd / cwnd;讲清慢启动与拥塞避免;理解 Reno / CUBIC / BBR 各自在怕什么;用 ss 等工具判断「慢在窗口还是慢在丢包」
导读:不是网卡坏了,是 TCP 在「踩刹车」
上一篇讲过:TCP 是可靠水管。但很多人会遇到这种体验------
刚开始下载很快往上冲
↓
忽然变慢、一顿一顿
↓
过一会儿又慢慢加速
或者:
Wi‑Fi 满格,网页却「半天才出来一块」
有线一插,世界清净了
第一反应常是:「带宽不够?服务器不行?」
有时确实如此;但还有一类非常常见的原因:
路径上出现了拥塞或丢包,TCP 的拥塞控制主动把发送速度降下来。
它不是在偷懒,而是在避免「大家一起把路堵死」。
本专题把 滑动窗口、流量控制、拥塞控制 放到一张图里讲透,再落到 Linux 上怎么观察。

一、概念:三扇「窗户」决定你能跑多欢
1.1 先分清两件怕的事
| 流量控制(Flow Control) | 拥塞控制(Congestion Control) | |
|---|---|---|
| 怕谁忙不过来 | 接收方应用 / 接收缓冲 | 整条网络路径(路由器队列、瓶颈链路) |
| 信号从哪来 | 对端通告的 rwnd(接收窗口) | 丢包、重复 ACK、RTT 变化等(推断) |
| 调什么 | 别把对方「灌满」 | 调本地的 cwnd(拥塞窗口) |
| 类比 | 货架满了,别再送货 | 高速堵车了,大家一起减速 |
一句话:
流量控制:护的是「对面那个人」
拥塞控制:护的是「中间那条路」
1.2 发送方真正能发多少?
概念层(忽略细节差异)可以记:
发送窗口 ≈ min(rwnd, cwnd)
再加一个生活化量:
| 名词 | 人话 |
|---|---|
| rwnd | 对方说:我货架还剩这么大 |
| cwnd | 我认为:路上还能塞这么多「在途货」比较安全 |
| 飞行中的数据(bytes in flight) | 已发出、还没被 ACK 确认的字节 |
| BDP(带宽时延积) | 理想情况下「管道里」大概能装多少:≈ 带宽 × RTT |
已确认 已发送未确认(in flight) 窗口内还可发
██████████░░░░░░░░░░░░░░░░░░░░│░░░░░░░░░░░░░░░░│
▲ ▲
左沿(SND.UNA) 右沿受 min(rwnd,cwnd) 限制
吞吐要上去,窗口通常得够大;但窗口乱开大,丢包和延迟会反噬。
拥塞控制,就是在这条钢丝上跳舞。
1.3 滑动窗口在干什么?
TCP 用 序号 + ACK + 窗口 实现「流水线发送」:不必一发一等,可以一口气发出一串,再根据 ACK 往前「滑」。
时间 →
发: [1][2][3][4]...... (窗口允许范围内连续发)
收: ACK2 ACK3 ACK4 ......
窗口右沿不断右移 → 可以继续发后面的
没有窗口,可靠传输会退化成「停等协议」,高速链路利用率极低。
有了窗口,才配得上「可靠水管」这个外号。
1.4 拥塞控制到底在优化什么?
目标可以记成两句矛盾的愿望:
-
尽量吃满可用带宽(别浪费)
-
尽量别把共享瓶颈挤爆(别害人害己)
互联网是共享的:你家路由、运营商汇聚、跨洋链路,都有队列。
队列一满 → 延迟升、开始丢包 → 大家重传 → 更堵。这就是经典的 拥塞崩溃 风险。
所以 TCP 选择:根据网络反馈,动态调整 cwnd。
二、原理:从「慢启动」到现代算法
2.1 总览:一条连接的速度曲线长什么样?
经典「丢包反馈」家族(Reno 一脉)的故事线:
慢启动(指数涨)
↓ 到门限 ssthresh
拥塞避免(大致线性涨)
↓ 发生丢包 / 超时
猛踩刹车(窗口降下来)
↓
再爬坡......
cwnd
^
│ /\ /\
│ / \ / \
│ / \ / \
│ ____/ \/ \___
│ /
└──────────────────────────► 时间
慢启动 拥塞避免 丢包后恢复
弱网网页「一顿一顿」,常常对应:丢包 → 降窗 → 重传 → 再慢启动/恢复。
2.2 慢启动(Slow Start):名字叫慢,其实涨得很快
连接刚建立时,TCP 不知道 路径能扛多大。
策略:从较小的 cwnd 开始,每收到一轮成功的 ACK,大约把窗口翻倍(指数增长)------像试水温。
cwnd: 1 → 2 → 4 → 8 → 16 → ...
(单位常以 MSS 段数理解;实现细节因栈而异)
为什么叫「慢」?相对「一上来就按链路带宽狂发」而言,它是保守试探;
但对用户体感,前几百毫秒往往是 涨速最快 的阶段。
退出慢启动的常见条件:
-
cwnd 涨到 ssthresh(慢启动门限)→ 进入拥塞避免
-
或发生拥塞信号(丢包等)→ 调整 ssthresh / cwnd
2.3 拥塞避免(Congestion Avoidance):改成「探一步」
进入拥塞避免后,不再指数猛涨,而大致变成:
每个 RTT,cwnd 大约 +1 个 MSS (加法增)
这就是经典 AIMD 思想里的 AI(Additive Increase) 部分:
慢慢试探还有没有余量,避免一下子把路踩爆。
2.4 丢包了怎么办?超时 vs 快速重传
(1)超时重传(RTO)
很久都等不到 ACK → 可能网络很糟糕。
经典处理更狠:窗口降得很低,常常重新走「谨慎爬升」。
体感:卡很久、吞吐掉崖。
(2)快速重传(Fast Retransmit)
若连续收到 多个重复 ACK(常见阈值是 3 个 dup ACK),更像「某段丢了,但后面的段还在到」:
发: 1 2 3 4 5
收: ACK2 ACK2 ACK2 ACK2 ... ← 说明 2 之后有东西到了,但缺 2?
↑ 重复 ACK
→ 立刻重传可能丢失的段,而不必死等超时
(3)快速恢复(Fast Recovery)
配合快速重传:不必把速度打回「新生儿」,而是 减半一类策略后继续发 (Reno 故事线)。
这对应 AIMD 的 MD(Multiplicative Decrease):乘法减,加法增------对共享链路相对公平。
2.5 一张表记住 Reno 家族的「情绪」
| 阶段 | cwnd 大致行为 | 心情 |
|---|---|---|
| 慢启动 | 指数升 | 我在摸路有多宽 |
| 拥塞避免 | 线性升 | 路好像还行,我轻轻加速 |
| 快速重传/恢复 | 降一批再继续 | 丢了一包,先让让 |
| 超时 | 降得很凶 | 路可能塌了,重来 |
注意:真实内核实现(CUBIC 等)细节更复杂,但 「探测 → 反馈 → 加/减」 的骨架不变。
2.6 Linux 上你更常听到的:CUBIC 与 BBR
CUBIC(长期常见的默认之一)
-
仍属 以丢包为主要拥塞信号 的大类
-
用三次函数曲线调窗,在 高带宽高延迟(长肥管道) 上往往比老 Reno 更会「填满管子」
-
体感:大文件传输、跨洋链路更「敢提速」
BBR(Bottleneck Bandwidth and RTT)
-
更关心 瓶颈带宽与最小 RTT,而不是「等到丢了再认怂」
-
试图把发送节奏调到:接近瓶颈能力,同时别把队列堆得老高
-
对 缓冲膨胀(bufferbloat) 场景常有更好延迟表现
-
但在某些「礼貌丢包 / 特殊策略」环境里,行为可能和丢包系算法「合不来」------工程上要实测
丢包系(Reno/CUBIC...): 路堵到「掉包」才强烈反应
BBR 系: 尽量用带宽与 RTT 模型主动调速
2.7 和「重传」的关系:可靠 ≠ 不减速
拥塞控制和可靠性机制是搭档:
丢包
├─ 可靠性:重传丢掉的数据(保证最终交付)
└─ 拥塞控制:降低 cwnd(保护路径,也保护自己后续吞吐)
所以你会看到一种「反直觉」:
TCP 为了长期更快、更稳,会在短期故意跑慢。
三、应用:这些现象其实是拥塞控制在说话
3.1 下载刚开始的「爬坡」
大文件 / 视频首片:
握手完成 → 慢启动爬坡 → 接近瓶颈 → 进入更平稳的相
短连接、小对象特别多时(老 HTTP/1.1 一堆小图):
每条连接都在爬坡,还没爬满就结束了 → 平均吞吐难看。
这也是后来连接复用、HTTP/2、QUIC 要解决的体感问题之一(下篇队头阻塞会再碰到)。
3.2 弱网「一顿一顿」
Wi‑Fi 干扰 → 突发丢包
→ 快速重传 / 降窗
→ 页面渲染卡住一下
→ 恢复后又继续
对比实验很有用:同一服务,有线 vs Wi‑Fi 。
若有线丝滑、Wi‑Fi 锯齿,优先怀疑接入质量触发拥塞控制,而不是业务代码写错。
3.3 数据中心与公网的差异
| 环境 | 常见特点 | 对拥塞控制的含义 |
|---|---|---|
| 同机房内网 | RTT 极低、带宽高 | 窗口很小也能跑高吞吐;算法差异有时不显眼 |
| 跨城 / 跨境 | RTT 大 | 需要更大窗口才能填满 BDP;算法选择更关键 |
| 移动网络 | 带宽跳变、抖动大 | 丢包与延迟信号都「吵」,体感波动大 |
3.4 应用层能做什么?(边界意识)
应用通常 不直接改 cwnd,但可以:
-
控制并发连接数与请求大小(别自己制造微突发洪水)
-
选合适协议与库(HTTP/2、QUIC)
-
做好超时与重试退避(避免重试风暴二次拥塞)
-
服务端限流、队列、过载保护(保护的是「业务拥塞」)
传输层拥塞控制:管「这条 TCP 连接别把网挤爆」
应用过载保护: 管「这个服务别被请求挤爆」
两者都叫「别堵死」,层级不同
四、问题定位:慢,是窗口小、丢包,还是对端不读?
4.1 先画决策树
「传数据很慢 / 吞吐上不去」
│
├─ 对端应用不 read? → 流量控制:rwnd 变小甚至 0(窗口关门)
│
├─ 路径丢包 / 重传高? → 拥塞控制降速 + 重传耗时
│
├─ RTT 很大且窗口不够? → 填不满 BDP(长肥管道)
│
└─ 本机 CPU / 磁盘 / 应用逻辑慢? → 根本不是网
4.2 推荐观察手段(Linux)
(1)看连接与重传相关信息
# 概览
ss -s
# 看 TCP 连接细节(不同内核字段略有差异)
ss -ti dst <对端IP>
# 系统级重传统计(名称因发行版而异,可 man nstat)
nstat -az | grep -i retrans
直觉:
| 线索 | 可能含义 |
|---|---|
| 重传计数持续涨 | 路径质量差或拥塞 |
| 发送队列堆着、吞吐低 | 窗口/拥塞/对端处理 |
| rwnd 很小 | 对端读得慢或缓冲压力 |
(2)抓包看 dup ACK / 重传
sudo tcpdump -ni any host <对端IP> and tcp -v
# 或用 Wireshark:Expert 里看 Retransmission / Dup ACK
看到成串 Duplicate ACK 再出现重传,很符合「快速重传」剧本。
(3)路径质量
ping <对端>
mtr -rwzbc 50 <对端> # 若已安装
丢包、RTT 剧烈抖动,会直接驱动拥塞控制「情绪化」。
4.3 两个经典误判
误判 1:把流量控制当成拥塞
接收方进程卡死 / 处理太慢
→ 通告 rwnd ↓
→ 发送方变慢
这是「对面货架满了」,不是「高速堵车」
误判 2:无限重试把路踩得更堵
客户端超时重试风暴
→ 更多包进入已拥塞路径
→ 更丢、更重传、更超时
要退避(backoff),不要无脑重试
4.4 最小实验(建立体感)
在可控环境用 tc 制造延迟/丢包(需权限,注意别在生产乱来):
# 示例:对某网卡加 50ms 延迟 + 1% 丢包(演示后记得删)
sudo tc qdisc add dev eth0 root netem delay 50ms loss 1%
# 用 iperf3 / 大文件下载观察吞吐锯齿
sudo tc qdisc del dev eth0 root
你会更直观地感到:同样「有带宽」,反馈一差,TCP 就会主动降速。
本章小结
滑动窗口让可靠传输能「流水线」跑起来
发送受限 ≈ min(rwnd, cwnd)
流量控制护接收方;拥塞控制护路径
慢启动指数试探,拥塞避免加法爬升
丢包 → 快重传/恢复或超时猛降
CUBIC 等擅长大带宽;BBR 更看带宽与 RTT
弱网一顿一顿,常常是降窗与重传的合奏
排障:重传、RTT、rwnd、有线对比、抓包 dup ACK