第 3 章:QUIC vs TCP——一张图看懂本质区别
从快递系统到网络协议
上一章我们用”寄快递”来类比 TCP。现在让我们把这个类比升级,用它来理解 QUIC 和 TCP 的根本差异。
想象两家快递公司:
TCP 快递公司(老牌、可靠、但规矩多):
- 寄东西之前必须先打电话确认对方在家(三次握手)
- 所有包裹走同一条运输线,按编号排队(单字节流)
- 如果中间有一个包裹丢了,后面所有包裹都在仓库等着(队头阻塞)
- 你搬家了?对不起,之前所有的物流单作废,重新来(无连接迁移)
- 包裹箱子是透明的,路上谁都能看到里面是什么(明文头部)
QUIC 快递公司(新兴、灵活、安全):
- 第一次合作只需要一句话确认(1-RTT),老客户直接发货(0-RTT)
- 每种货物走独立的运输线,互不影响(多流复用)
- A 类货物丢了一个,B 类货物继续正常送(无跨流阻塞)
- 你搬家了?没关系,我们认的是你的会员卡号不是地址(Connection ID)
- 所有包裹都上锁,只有你和收件人有钥匙(全程加密)
这就是 QUIC 和 TCP 最核心的差异。下面我们逐一展开。
总览对比图
┌─────────────────────────────────────────────────────────────────┐
│ 协议栈对比 │
├─────────────────────────────┬───────────────────────────────────┤
│ TCP + TLS 1.3 │ QUIC │
├─────────────────────────────┼───────────────────────────────────┤
│ ┌───────────────────────┐ │ ┌─────────────────────────────┐ │
│ │ HTTP/2 │ │ │ HTTP/3 │ │
│ ├───────────────────────┤ │ ├─────────────────────────────┤ │
│ │ TLS 1.3 │ │ │ QUIC │ │
│ ├───────────────────────┤ │ │ (传输+加密+多路复用 一体) │ │
│ │ TCP │ │ ├─────────────────────────────┤ │
│ ├───────────────────────┤ │ │ UDP │ │
│ │ IP │ │ ├─────────────────────────────┤ │
│ └───────────────────────┘ │ │ IP │ │
│ │ └─────────────────────────────┘ │
│ 4 层各管各的 │ QUIC 是一个"超级层" │
└─────────────────────────────┴───────────────────────────────────┘
关键区别:TCP 世界里,传输(TCP)和加密(TLS)是分开的两层,各有各的握手。QUIC 把这两件事合成了一层——这就是它能减少握手 RTT 的根本原因。
差异一:连接建立
TCP + TLS 1.3:最少 2 个 RTT
客户端 服务端
| |
|──────── TCP SYN ───────────────────────────>|
|<─────── TCP SYN+ACK ───────────────────────-| ← 1 RTT (TCP握手)
|──────── TCP ACK + TLS ClientHello ─────────>|
|<─────── TLS ServerHello + Finished ────────-| ← 2 RTT (TLS握手)
|──────── TLS Finished + HTTP Request ───────>|
|<─────── HTTP Response ─────────────────────-| ← 3 RTT (首字节)
| |
总计:2 RTT 后才能发送第一个应用数据
3 RTT 后收到第一个响应
注意:TCP Fast Open (TFO) 和 TLS 1.3 0-RTT 可以优化到 1-2 RTT,但在实践中部署率很低,因为中间设备不兼容。
QUIC:首次 1 RTT,重连 0 RTT
首次连接:
客户端 服务端
| |
|── Initial(ClientHello + 传输参数) ─────────>|
|<─ Initial(ServerHello) + Handshake ────────-| ← 1 RTT
|── Handshake + 1-RTT(HTTP请求) ────────────->|
|<─ 1-RTT(HTTP响应) ─────────────────────────-| ← 2 RTT (首字节)
| |
重连(0-RTT):
客户端 服务端
| |
|── Initial + 0-RTT(HTTP请求) ───────────────>| ← 第一个包就有数据!
|<─ Initial + Handshake + 1-RTT(HTTP响应) ───-| ← 1 RTT (首字节!)
| |
首次:1 RTT 后就能发应用数据
重连:0 RTT!第一个包就能带应用数据
为什么能做到?
QUIC 把传输握手和 TLS 握手合并了。TCP 需要先完成三次握手确认”连接建立”,然后才能在这个连接上做 TLS 握手。QUIC 的 Initial 包同时完成了两件事:确认对方存在(传输层)+ 交换加密参数(TLS)。
0-RTT 的原理:第一次连接成功后,服务端会给客户端一个”会话票据”(Session Ticket)。下次客户端用这个票据,不需要重新协商加密参数,直接用上次协商好的密钥加密数据。
0-RTT 的代价:
0-RTT 数据有重放攻击风险——攻击者可以截获 0-RTT 数据包并重新发送。因此 0-RTT 只适合幂等操作(GET 请求等),不适合有副作用的操作(POST 转账等)。这是安全性和速度的 trade-off。
差异二:数据传输模型
TCP:单一字节流
TCP 把所有数据视为一条连续的字节流。应用层写入的数据,TCP 负责按顺序、可靠地交付给对端。
应用层写入:
write("Hello") → 字节 0-4
write("World") → 字节 5-9
write("Foo") → 字节 10-12
TCP 看到的:
[H][e][l][l][o][W][o][r][l][d][F][o][o]
0 1 2 3 4 5 6 7 8 9 10 11 12
←────────── 一条连续的字节流 ──────────→
网络上的 TCP 段:
Segment 1: SEQ=0, 数据="Hello"
Segment 2: SEQ=5, 数据="World" ← 假设这个丢了
Segment 3: SEQ=10, 数据="Foo" ← 已到达但不能交给应用
应用层读取:
read() → "Hello" (可以读)
read() → 阻塞等待... (字节5-9还没到)
即使 "Foo" 已经在内核缓冲区里了,也读不到!
这就是 TCP 队头阻塞的根本原因:单一的序列号空间决定了所有数据必须有序交付。
QUIC:独立的多流
QUIC 原生支持多个流(Stream),每个流是独立的字节流,有自己的序列号空间:
应用层写入:
Stream 0: write("CSS data")
Stream 4: write("JS code")
Stream 8: write("Image bytes")
QUIC 看到的:
Stream 0: [C][S][S][ ][d][a][t][a] 独立的字节流
Stream 4: [J][S][ ][c][o][d][e] 独立的字节流
Stream 8: [I][m][a][g][e] 独立的字节流
网络上的 QUIC 包:
Packet 1: {Stream 0, offset=0, "CSS "} + {Stream 4, offset=0, "JS"}
Packet 2: {Stream 8, offset=0, "Imag"} ← 假设这个丢了
Packet 3: {Stream 0, offset=4, "data"} + {Stream 4, offset=2, " code"}
应用层读取:
Stream 0: read() → "CSS data" ✓ 完整到达,直接交付!
Stream 4: read() → "JS code" ✓ 完整到达,直接交付!
Stream 8: read() → 等待重传... (只有这个流被阻塞)
关键区别:Stream 8 的丢包不影响 Stream 0 和 Stream 4 的交付。在 HTTP/3 中,CSS、JS、图片各自使用不同的 Stream,一个资源的丢包不会卡住其他资源。
流的类型和编号
QUIC 流通过 ID 标识,ID 的最低 2 位有特殊含义:
Stream ID 最低2位:
0b00 (0, 4, 8, ...) → 客户端发起的双向流
0b01 (1, 5, 9, ...) → 服务端发起的双向流
0b10 (2, 6, 10, ...) → 客户端发起的单向流
0b11 (3, 7, 11, ...) → 服务端发起的单向流
这个设计很巧妙——不需要额外的协商就能判断一个流是谁发起的、是单向还是双向。
差异三:包号与确认机制
TCP:序列号可复用
TCP 的 SEQ(序列号)表示”这是字节流中第几个字节”。重传时,同一个 SEQ 会被重新使用:
TCP 重传场景:
发送: SEQ=1000, 数据="abc" (第一次)
超时,没收到 ACK
重传: SEQ=1000, 数据="abc" (重传,同一个SEQ!)
问题:收到 ACK=1003 时,无法判断是在确认第一次发送还是重传
→ RTT 估算不准确(重传歧义问题)
QUIC:包号单调递增
QUIC 的 Packet Number 是单调递增的,永远不会重复。重传的数据会用新的包号:
QUIC 重传场景:
发送: Packet 100, 数据="abc" (第一次)
超时,没收到 ACK
重传: Packet 150, 数据="abc" (新包号!)
收到 ACK 确认 Packet 150 时:
→ 明确知道是在确认重传的那个包
→ RTT = 当前时间 - Packet 150 的发送时间
→ 精准!
为什么这很重要?
准确的 RTT 估算是拥塞控制的基础。如果 RTT 估错了,拥塞控制算法就会做出错误决策——要么太保守(浪费带宽),要么太激进(导致更多丢包)。
ACK 机制的差异
TCP 使用累积确认(Cumulative ACK):ACK=1000 表示”字节 1000 之前的都收到了”。
QUIC 使用 ACK Range:明确列出收到了哪些包号范围:
QUIC ACK Frame:
Largest Acknowledged: 150
ACK Ranges: [100-120], [130-140], [145-150]
意思是:包 100-120、130-140、145-150 都收到了
包 121-129、141-144 没收到(可能丢了)
这让发送端能精确知道哪些包丢了,而不是像 TCP 那样通过 SACK(选择性确认)来猜测。
差异四:加密范围
TCP:明文头部
TCP 段格式(简化):
┌───────────────────────────────────────┐
│ 源端口 (16bit) │ 目标端口 (16bit) │ ← 明文
├───────────────────────────────────────┤
│ 序列号 (32bit) │ ← 明文
├───────────────────────────────────────┤
│ 确认号 (32bit) │ ← 明文
├───────────────────────────────────────┤
│ 头长│保留│标志│ 窗口大小 (16bit) │ ← 明文
├───────────────────────────────────────┤
│ 校验和 │ 紧急指针 │ ← 明文
├───────────────────────────────────────┤
│ 数据 │ ← TLS加密(如果用了HTTPS)
└───────────────────────────────────────┘
中间设备能看到:端口、序列号、窗口大小、标志位
→ 防火墙可以根据这些信息做决策
→ 运营商可以做"TCP优化"(篡改窗口大小等)
→ 协议骨化:改不动了
QUIC:几乎全部加密
QUIC 包格式(1-RTT 包,简化):
┌─────────────────────────────────┐
│ Header Form (1bit) │ ← 明文(必须的)
│ Fixed Bit (1bit) │ ← 明文
│ Spin Bit (1bit) │ ← 明文(用于RTT测量)
│ Reserved (2bit) │ ← 加密保护
│ Key Phase (1bit) │ ← 加密保护
│ Packet Number Length (2bit) │ ← 加密保护
├─────────────────────────────────┤
│ Destination Connection ID │ ← 明文(路由需要)
├─────────────────────────────────┤
│ Packet Number (1-4 bytes) │ ← 头部保护(加密)
├─────────────────────────────────┤
│ │
│ Payload │ ← 完全加密
│ (STREAM frames, │
│ ACK frames, │
│ 所有控制信息...) │
│ │
└─────────────────────────────────┘
中间设备能看到:几乎只有 Connection ID
→ 无法读取序列号(包号被加密)
→ 无法看到 ACK 信息
→ 无法做"优化"或篡改
→ 协议可以自由演进!
Header Protection(头部保护):
QUIC 有一个独特的设计——用包内容的一部分来加密包号。这叫 Header Protection。它的目的是让攻击者无法通过观察包号变化来推断流量模式。
加密前:[Header] [Packet Number] [Encrypted Payload]
↑ 明文
Header Protection 后:
1. 取 Payload 前几个字节作为 mask
2. 用 mask XOR Packet Number 的字节
结果:[Header] [Protected PN] [Encrypted Payload]
↑ 无法直接读取
差异五:连接标识
TCP:四元组绑定
TCP 连接标识 = (源IP, 源端口, 目的IP, 目的端口)
场景:手机从 WiFi 切到 4G
WiFi 下: (192.168.1.100, 12345, 93.184.216.34, 443)
4G 下: (10.0.0.50, 54321, 93.184.216.34, 443)
↑ 变了 ↑ 变了
服务端:这是两个完全不同的连接!
→ 旧连接超时关闭
→ 新连接需要重新握手(TCP 3次 + TLS)
→ 应用层状态丢失(正在下载的文件、视频播放位置等)
QUIC:Connection ID
QUIC 连接标识 = Connection ID(一个随机的字节序列)
场景:手机从 WiFi 切到 4G
WiFi 下: UDP(192.168.1.100:12345) + CID=0x1a2b3c4d
4G 下: UDP(10.0.0.50:54321) + CID=0x1a2b3c4d
↑ 变了 ↑ 变了 ↑ 没变!
服务端:CID 对上了,还是同一个连接
→ 不需要重新握手
→ 应用层状态完整保留
→ 用户无感知切换
Connection ID 的高级用法:
实际上,QUIC 连接会有多个 Connection ID。客户端和服务端各自可以为连接分配多个 CID:
为什么需要多个 CID?
隐私保护:
如果永远用同一个 CID,网络窃听者可以追踪你的移动轨迹:
"CID=0x1a2b 先出现在 WiFi,然后出现在 4G——是同一个人在移动!"
解决方案:
每次网络切换时使用新的 CID:
WiFi: CID=0x1a2b3c4d
4G: CID=0x5e6f7a8b ← 不同的 CID!
窃听者看不出这是同一个连接
但服务端内部知道:两个 CID 都映射到同一个连接状态
差异六:多路复用的层次
HTTP/2 over TCP:应用层多路复用,传输层单流
HTTP/2 + TCP 的现实:
应用层(HTTP/2)认为自己有多个流:
Stream 1: GET /index.html
Stream 3: GET /style.css
Stream 5: GET /image.png
但 TCP 只看到一条字节流:
[S1数据][S3数据][S5数据][S1数据][S3数据]...
TCP 丢了一个包 → 所有 HTTP/2 Stream 全部阻塞
讽刺:HTTP/2 的多路复用设计,在 TCP 上反而可能比
HTTP/1.1 的多连接(每个资源一条连接)更慢!
HTTP/3 over QUIC:传输层原生多流
HTTP/3 + QUIC 的现实:
QUIC 传输层原生支持多个流:
Stream 0: GET /index.html ← 独立序列号空间
Stream 4: GET /style.css ← 独立序列号空间
Stream 8: GET /image.png ← 独立序列号空间
QUIC 丢了 Stream 8 的一个包:
Stream 0: 继续交付 ✓
Stream 4: 继续交付 ✓
Stream 8: 等待重传 ✗ (只有这个阻塞)
终于实现了"真正的"多路复用!
差异七:拥塞控制的灵活性
TCP:内核固化,升级困难
TCP 的拥塞控制实现在操作系统内核中。要换一个拥塞控制算法(比如从 CUBIC 换到 BBR),你需要:
- 升级操作系统内核(或加载内核模块)
- 修改系统配置
- 重启服务
而且不同客户端(Windows/macOS/Linux/iOS/Android)各有各的内核实现,行为可能不同。
QUIC:用户态实现,随应用更新
QUIC 运行在用户态(不是内核里),拥塞控制作为应用的一部分:
TCP 拥塞控制部署:
需要升级 OS 内核 → 全球设备升级周期以年计
QUIC 拥塞控制部署:
应用自带 → 更新 App 就能用新算法
A/B 测试 → 同一服务器对不同用户用不同算法
自定义 → 可以根据业务特点定制
这就是为什么 QUIC 实现里拥塞控制的多样性远超 TCP:
| 算法 | TCP 支持 | QUIC 支持 |
|---|---|---|
| CUBIC | Linux/Windows/macOS 默认 | quiche, s2n-quic, TQUIC |
| BBR | Linux 4.9+ | quiche (Chromium port), TQUIC |
| BBR2 | Linux 5.x+ 实验 | quiche |
| BBR3 | Linux 6.x+ | TQUIC (唯一) |
| COPA | 未进入主线内核 | TQUIC (唯一) |
差异八:可演进性(Anti-Ossification)
TCP 的骨化教训
过去 20 年,多少 TCP 扩展失败了:
失败案例:
- TCP Fast Open (TFO):~7% 的网络路径上被中间设备阻断
- ECN (显式拥塞通知):部署了 20+ 年,实际使用率仍然很低
- 新的 TCP Option:很多防火墙会丢弃包含"不认识"选项的包
- Multipath TCP (MPTCP):中间设备可能修改 TCP 头部,破坏完整性
根本原因:TCP 头部是明文的,中间设备可以检查和修改
QUIC 的防骨化设计
QUIC 从设计之初就把”防止骨化”作为核心目标:
策略1:加密一切
→ 中间设备看不到内部结构,无法形成依赖
策略2:GREASE(随机生成无意义的扩展)
→ 如果中间设备遇到不认识的值就丢包,
那部署 GREASE 后这些设备会被发现并修复
→ 防止"未来扩展被阻断"的问题
策略3:Version Negotiation
→ 协议版本号可以更新(已有 QUIC v2 = RFC 9369)
→ 新版本可以改变任何加密后的内部格式
策略4:最少暴露
→ 只暴露路由必需的信息(Connection ID)
→ Spin Bit 是唯一允许的可观测性让步(用于 RTT 测量)
综合对比表
| 维度 | TCP | QUIC |
|---|---|---|
| 所在层 | 内核传输层 | 用户态(UDP 之上) |
| 首次连接 | 2-3 RTT (TCP+TLS) | 1 RTT |
| 重复连接 | 1-2 RTT (TCP+TLS恢复) | 0 RTT |
| 数据模型 | 单一字节流 | 多独立流 |
| 队头阻塞 | 有(流间+流内) | 仅流内 |
| 包号 | 可复用(重传歧义) | 单调递增(精确 RTT) |
| 确认方式 | 累积 ACK + SACK | ACK Range |
| 加密 | 可选(TLS 是上层) | 强制(内置 TLS 1.3) |
| 头部保护 | 无(明文) | 有(Header Protection) |
| 连接标识 | 四元组 | Connection ID |
| 连接迁移 | 不支持 | 原生支持 |
| 拥塞控制 | 内核实现 | 用户态,可插拔 |
| 可演进性 | 骨化严重 | 防骨化设计 |
| NAT 穿透 | 依赖端口映射 | CID 不依赖端口 |
| 中间设备干预 | 常见且难以避免 | 几乎无法干预 |
代码视角:quinn 中的 Stream vs TCP Socket
让我们看看在代码层面,QUIC 的多流是怎么体现的:
// TCP: 一个连接就是一个 socket,读写都在同一个流上
let tcp_stream = TcpStream::connect("server:443").await?;
tcp_stream.write_all(b"request 1").await?; // ← 写入唯一的字节流
tcp_stream.write_all(b"request 2").await?; // ← 还是同一个字节流
let response = tcp_stream.read(&mut buf).await?;
// QUIC (quinn): 一个连接可以开多个流
let connection = endpoint.connect(addr, "server")?.await?;
// 并行打开多个流,互不影响
let (mut send1, recv1) = connection.open_bi().await?; // ← 流1
let (mut send2, recv2) = connection.open_bi().await?; // ← 流2
send1.write_all(b"request on stream 1").await?; // ← 独立!
send2.write_all(b"request on stream 2").await?; // ← 独立!
// 流1的丢包不会阻塞流2的读取
核心区别:TCP 是”一个连接一个流”,QUIC 是”一个连接多个流”。HTTP/2 在应用层模拟了多流(但底层还是 TCP 的单流),HTTP/3 则使用 QUIC 的原生多流。
代码视角:Connection ID 在 quiche 中的实现
// quiche 中连接的创建(简化)
pub fn accept(
scid: &ConnectionId, // ← 服务端选择的 Connection ID
odcid: Option<&ConnectionId>,
local: SocketAddr,
peer: SocketAddr,
config: &mut Config,
) -> Result<Connection> {
// Connection ID 是连接的唯一标识
// 注意:这里虽然也传入了 local/peer 地址,
// 但地址变化时连接不会断开——靠 CID 识别
}
// 当客户端 IP 变化时(连接迁移)
// quiche 内部会:
// 1. 发现包的源地址变了
// 2. 但 CID 匹配 → 还是同一个连接
// 3. 验证路径(发 PATH_CHALLENGE)
// 4. 确认新路径可用后继续传输
实际性能差异:数据说话
Google 在 2017 年的论文中给出了对比数据(SIGCOMM 2017):
场景:YouTube 视频播放
网络条件 TCP 重缓冲率 QUIC 重缓冲率 改善
─────────────────────────────────────────────────────
丢包率 1% 2.3% 1.6% 30% ↓
丢包率 3% 7.1% 4.8% 32% ↓
丢包率 5% 12.4% 8.2% 34% ↓
搜索首字节时间 (TTFB):
网络条件 TCP TTFB QUIC TTFB 改善
─────────────────────────────────────────────────────
良好网络 ~50ms ~35ms 30% ↓
中等网络 ~200ms ~120ms 40% ↓
弱网 ~800ms ~400ms 50% ↓
关键发现:网络越差,QUIC 的优势越明显。在良好网络下差异较小,因为 TCP 的缺陷被掩盖了。
但 QUIC 不是银弹
QUIC 的劣势和挑战
1. CPU 开销更高
QUIC 在用户态实现,加密/解密每个包都需要 CPU 参与。TCP 在内核态有硬件卸载(TSO、GRO、checksum offload)的支持:
TCP: 应用 → 内核 → 网卡(硬件加速)
QUIC: 应用(加密+封装) → 内核 → 网卡
QUIC 的额外开销:
- 每个包的 AEAD 加密/解密
- Header Protection 的加密/解密
- 用户态到内核态的上下文切换更多
这就是为什么 msquic 要做 XDP(内核旁路)、s2n-quic 要做 io_uring——都在优化这个瓶颈。
2. UDP 可能被阻断
有些企业防火墙、老旧路由器会阻断 UDP 流量。QUIC 实现通常需要一个回退机制:如果 UDP 不通,退回到 TCP+TLS。
QUIC 连接尝试:
1. 尝试 QUIC (UDP:443)
2. 如果超时(可能被阻断)
3. 回退到 HTTP/2 (TCP:443)
实际数据:约 2-5% 的网络环境无法使用 QUIC
3. 实现复杂度高
QUIC 把 TCP + TLS + HTTP/2 的多路复用合在一起,实现难度远超单独的 TCP:
协议复杂度对比:
TCP (RFC 793): ~85 页
TLS 1.3 (RFC 8446): ~160 页
QUIC 核心 (RFC 9000): ~151 页
QUIC TLS (RFC 9001): ~52 页
QUIC 恢复 (RFC 9002): ~42 页
─────────────────────────────
QUIC 总计: ~245 页(且相互交织)
4. 调试更困难
TCP 是明文的,Wireshark 直接能看到所有细节。QUIC 加密后,你需要密钥才能解密看到内部信息。虽然有 SSLKEYLOGFILE 机制(和 TLS 一样),但确实增加了调试难度。
什么时候该用 QUIC,什么时候 TCP 够用?
| 场景 | 推荐 | 原因 |
|---|---|---|
| 公网 Web 应用 | QUIC | 丢包改善、0-RTT、连接迁移 |
| 移动端 App | QUIC | 网络切换频繁,连接迁移是刚需 |
| 弱网/高丢包 | QUIC | 无跨流 HOL,性能优势显著 |
| 内网微服务 | TCP | 网络好、延迟低,QUIC 优势不明显 |
| 数据库连接 | TCP | 长连接、低丢包,TCP 更成熟 |
| 需要硬件加速 | TCP | 网卡 offload 支持更完善 |
| 低 CPU 资源设备 | TCP | QUIC 加密开销更大 |
本章小结
| 维度 | TCP 的设计 | QUIC 的设计 | 为什么 QUIC 更好 |
|---|---|---|---|
| 连接建立 | 分层握手 | 合并握手 | 少 1-2 个 RTT |
| 数据模型 | 单字节流 | 多独立流 | 消除跨流 HOL |
| 包号 | 可复用 | 单调递增 | 精确 RTT 估算 |
| 加密 | 可选/分层 | 强制/内置 | 防骨化+隐私 |
| 连接标识 | IP+端口 | Connection ID | 支持迁移 |
| 升级部署 | 改内核 | 改应用 | 快速迭代 |
一句话总结:QUIC 通过在用户态重新设计传输层,解决了 TCP 40 年积累的三大问题(HOL 阻塞、握手延迟、无法迁移),同时通过全程加密保持了协议的可演进性。
读完本章你能做什么
- 画出 TCP+TLS vs QUIC 的连接建立时序图,标注 RTT 数
- 解释为什么 HTTP/2 的多路复用在 TCP 上是”伪多路复用”
- 说出 QUIC 包号单调递增对 RTT 估算的好处
- 解释 Connection ID 如何实现连接迁移
- 列出 QUIC 相比 TCP 的三个劣势
- 给出”什么场景用 QUIC、什么场景用 TCP”的判断标准
常见误区
误区一:QUIC 完全没有队头阻塞
正确理解:QUIC 消除了跨流的队头阻塞,但流内的队头阻塞仍然存在。如果 Stream 4 的第一个包丢了,Stream 4 内部后续的数据仍然要等重传。区别在于:Stream 0 和 Stream 8 不受影响。
误区二:0-RTT 可以用在所有场景
正确理解:0-RTT 数据有重放攻击风险。攻击者可以截获 0-RTT 包并重发,让服务器执行两次同样的操作。因此 0-RTT 只应该用于幂等操作(GET、HEAD),不能用于有副作用的操作(POST 转账、下单)。服务端需要实现重放检测或限制 0-RTT 只接受幂等请求。
误区三:QUIC 比 TCP 快是因为用了 UDP
正确理解:UDP 本身不提供任何性能优势——它只是一个”空壳”,让 QUIC 有地方放自己的协议。QUIC 快的原因是更好的连接建立(合并握手)、消除跨流 HOL、以及更先进的拥塞控制实现。QUIC 在 UDP 之上做的可靠性保证一点都不比 TCP 少。
导读目录:README.md 上一章:第 2 章 · 阅读路线图与前置知识 下一章:第 4 章 · 连接建立与握手——从陌生人到老朋友