适用读者:知道"TCP 可靠"但说不清"靠什么可靠"的人;被"偶发超时""吞吐上不去"困扰过的人。
前置:S02-B03(握手与首部字段)。
实测环境:Ubuntu 24.04.1 / Linux 6.8 / iproute2 +tc netem;实测日期 2026-11-05。
覆盖旧文:A-1035(TCP 如何保证可靠传输,101 点)、A-1038(TCP 与 UDP 的区别,89 点)= 190 点。
覆盖扩展:E-S02-006(拥塞控制)、E-S02-008(QUIC)。
上一篇:S02-B03 · TCP 三次握手与四次挥手:为什么是三次不是两次
下一篇:S02-B05 · 粘包与拆包。
1. 一句话结论
TCP 的可靠性来自四件事:序号定位、确认回执、超时重传、窗口限流。 四者缺一不可。
| 机制 | 解决什么 | 对应首部字段 |
|---|---|---|
| 序号(seq) | 给每个字节编号 → 能发现丢/乱 | seq |
| 确认(ack) | 告诉对方"我收到了到第几字节" | ack |
| 重传 | 没收到确认就再发 | RTO、SACK |
| 窗口 | 别发太快把我撑爆 | window、Window Scale |
| (另加)拥塞控制 | 别发太快把网络撑爆 | cwnd |
"乱"和"重"的处理方式不一样:乱序靠序号重排(接收缓冲区),重复靠序号去重(已收的直接丢弃并再确认)。
2. 实测:丢包对 TCP 有多致命
2.1 无损基线
本机 loopback 传 12.5 MB:
[无损] 接收 12.50 MB,耗时 0.03s,吞吐 472.3 MB/s
2.2 注入 20% 丢包后(同一个程序、同一个方向)
tc qdisc add dev lo root netem loss 20%
接收 12.50 MB,耗时 46.80s
新增重传段 = 67
| 场景 | 耗时 | 吞吐 |
|---|---|---|
| 无损 | 0.03 s | 472 MB/s |
| 20% 丢包 | 46.80 s | 0.27 MB/s |
吞吐掉了约 1,700 倍。 这不是夸张,是 TCP 的设计后果:
- 丢包 → 触发重传(RTO 通常是几百毫秒级);
- 丢包被当作"网络拥塞"信号 → 拥塞窗口被砍半甚至回到慢启动;
- 于是"丢一个包,整体速度断崖式下降"。
这条实测解释了两个真实故障:
| 现象 | 原因 |
|---|---|
| "带宽明明够,为什么慢" | 有丢包,TCP 自己在限速 |
| "长距离跨国调用特别慢" | RTT 大 + 轻微丢包,吞吐被 BDP 与拥塞控制双重限制 |
3. 四个机制逐一拆
3.1 序号与确认:连续字节流的记账
发送方 接收方
seq=1000, len=100 ───────────────► 收到 1000–1099,回 ack=1100(期望下一个字节)
seq=1100, len=100 ────── 丢了 ───X
seq=1200, len=100 ───────────────► 收到 1200–1299,但 1100–1199 缺 → 回 ack=1100(重复确认)
重复 ACK 是关键信号:收到 3 个对同一位置的重复 ACK → 快重传(不必等超时)。
3.2 超时重传与 RTO
- RTO(Retransmission Timeout)根据 RTT 动态计算(见 B03 的
ss -ti里rto:204)。 - RTO 太小 → 无谓重传(浪费带宽);太大 → 丢包后长时间卡住。
- RTO 的最小值在 Linux 上是 200 ms 级——所以任何一次超时重传,至少损失 200 ms。
这就是"高延迟链路最怕丢包"的量化解释:一次重传 = 200 ms 起。
3.3 流量控制(接收方别被撑爆)
接收方在 ACK 里带 window(还能收多少字节)。发送方受此限制。
窗口为 0 时:发送方停止,定期发"窗口探测"询问。
3.4 拥塞控制(网络别被撑爆)
| 阶段 | 行为 |
|---|---|
| 慢启动 | cwnd 从 1 开始指数增长(每 RTT 翻倍) |
| 拥塞避免 | 到阈值后改为线性增长 |
| 快重传/快恢复 | 3 个重复 ACK → 立即重传,cwnd 减半 |
| 超时 | cwnd 回到 1(最狠的惩罚) |
算法演进:Cubic(Linux 默认,实测 ss -ti 里显示 cubic)→ BBR(基于带宽与 RTT 建模,弱网表现更好)。
3.5 BDP:为什么"窗口小"会限制吞吐
吞吐上限 ≈ 窗口大小 / RTT
| 场景 | 窗口 | RTT | 理论吞吐 |
|---|---|---|---|
| 内网 | 64 KB | 0.2 ms | 极大(不是瓶颈) |
| 同城 | 64 KB | 5 ms | 12.8 MB/s |
| 跨国 | 64 KB | 200 ms | 0.32 MB/s |
| 跨国(窗口放大到 4 MB) | 4 MB | 200 ms | 20 MB/s |
所以 B03 里说的 Window Scale 选项不是可选项——没有它,长肥管道(long fat network)上吞吐上不去。
4. UDP:把复杂性交回应用层
| 维度 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接(握手) | 无连接 |
| 可靠性 | 确认 + 重传 | 不保证 |
| 顺序 | 保证有序 | 不保证 |
| 边界 | 字节流(无消息边界) | 数据报(保留边界) |
| 首部 | 20 字节起 | 8 字节 |
| 拥塞控制 | 有(自动降速) | 无(可以打满,也可能打死网络) |
| 适用 | 网页、API、数据库连接 | 音视频、游戏、DNS、QUIC |
4.1 UDP 什么时候更好
| 场景 | 为什么 UDP |
|---|---|
| 实时音视频 | 宁可丢一帧也不要等重传(重传来的帧已经过期) |
| 游戏 | 位置更新只关心"最新",旧包无用 |
| DNS | 一问一答,包很小,握手成本占比高 |
| QUIC / HTTP3 | 在 UDP 上自己实现可靠传输 + 多路复用(E-S02-008) |
判据:"迟到的数据还有用吗?" 没用 → UDP(或 QUIC);有用 → TCP。
4.2 QUIC 的思路(E-S02-008)
QUIC = UDP + 用户态实现的可靠性 + 加密 + 多路复用:
| 特性 | 解决 TCP 的什么问题 |
|---|---|
| 0-RTT / 1-RTT 握手 | TCP+TLS 要两轮,QUIC 合并 |
| 流级多路复用 | TCP 的队头阻塞(一个包丢,所有流都等) |
| 连接迁移(Connection ID) | 换 WiFi/4G 时 TCP 连接会断,QUIC 不断 |
| 用户态实现 | 升级不用改内核 |
代价:UDP 在部分企业网络被封;CPU 开销高于内核 TCP。
5. 排障:怎么判断"是网络慢还是应用慢"
# ① 看连接级指标(RTT、重传、拥塞窗口)
ss -ti state established
# ② 看全系统重传统计
nstat -az | grep -iE "TcpRetransSegs|TcpExtTCPLostRetransmit"
# ③ 看丢包与错误
ip -s link show <网卡> # RX/TX errors, dropped
| 指标 | 正常 | 异常含义 |
|---|---|---|
retrans 占比 |
< 0.1% | 高 → 链路质量差或拥塞 |
rtt |
稳定 | 抖动大 → 路由不稳/中间设备排队 |
cwnd |
随流量增长 | 长期很小 → 一直被判拥塞 |
J01 会把这套流程做成完整的排障 SOP(含 tcpdump 替代方案、内核参数调优)。
6. 五个误区
| 误区 | 真相 |
|---|---|
| "TCP 可靠 = 应用不会丢数据" | 只保证"字节流到达",连接断了照样丢;应用层仍需确认与幂等 |
| "UDP 不可靠所以不能用" | 音视频/游戏/DNS/QUIC 大量使用;不可靠≠不好用 |
| "带宽够就不慢" | RTT 与丢包才是主因(§3.5 的 BDP) |
| "重传是异常" | 轻微重传正常;持续 > 1% 才要查 |
| "关掉拥塞控制能更快" | 短期快,长期造成全局拥塞崩塌(还会被中间设备限速) |
7. 自测
- 三个重复 ACK 触发什么?比等超时重传快多少(量级)?
- 吞吐上限公式是什么?跨国 200 ms RTT、窗口 64 KB,理论吞吐多少?
- 为什么音视频更愿意用 UDP?
- QUIC 用 UDP 实现,解决了 TCP 的哪三个具体问题?
- 线上"带宽够但慢",你会先看哪三个指标?
8. 下一篇
S02-B05 · 粘包与拆包:应用层为什么必须自己划边界
可靠传输保证"字节都到了、顺序也对",但它不保证"你的两条消息是两次 recv"。下一篇用一段真实代码复现"hello + world 变成一次收到 helloworld",并给出三种划边界方案与判定方法。
本篇属于系列《S02 · 计算机与网络基础》。旧文来源:A-1035、A-1038;扩展点 E-S02-006/008。丢包对照(472 MB/s vs 0.27 MB/s)为 2026-11-05 实测。