QUIC 传输层
流与流 ID、变长整数、1-RTT/0-RTT 握手、连接 ID 与连接迁移。
RFC 9000(传输)、9001(TLS)、9002(丢包检测与拥塞控制)。这一篇只讲 HTTP/3 用得到的部分。
流
QUIC 原生提供多条独立的有序字节流,这是它与 TCP 的根本差异:
| TCP | QUIC | |
|---|---|---|
| 抽象 | 一条有序字节流 | 多条独立有序流 |
| 某段丢失时 | 全部后续数据阻塞 | 只阻塞该流 |
| 流量控制 | 连接级 | 流级 + 连接级 |
| 流的创建成本 | 一次握手 | 零(发第一个字节即创建) |
关键:QUIC 知道每个字节属于哪个流。丢失的包只影响它所承载的那些流,其他流的数据照常交付给应用。
流 ID
62 位变长整数,最低两位编码流的类型:
| 低 2 位 | 类型 | HTTP/3 用途 |
|---|---|---|
0x00 | 客户端发起,双向 | 请求流(一个请求/响应一条) |
0x01 | 服务端发起,双向 | h3 不使用 |
0x02 | 客户端发起,单向 | 控制流、QPACK 编码器/解码器流 |
0x03 | 服务端发起,单向 | 控制流、QPACK 流、推送流 |
对照 h2 的流 ID 规则(奇偶分客户端/服务端):QUIC 多了「单向/双向」这一维,且 h2 的流 0(连接级)不存在——连接级控制在 QUIC 帧层或 h3 控制流上。
变长整数
QUIC 与 HTTP/3 的所有整数都用这套编码,首字节的高 2 位决定总长度:
| 前 2 位 | 总字节数 | 可表示范围 |
|---|---|---|
00 | 1 | 0 – 63 |
01 | 2 | 0 – 16383 |
10 | 4 | 0 – 2^30−1 |
11 | 8 | 0 – 2^62−1 |
去掉前缀的剩余位即数值(大端)。例:0x25 = 00100101 → 1 字节、值 37。
好处:小数值(绝大多数流 ID、帧类型、长度)只占 1 字节,同时保留了 62 位的表达上限。对照 h2 的定长 9 字节帧头——h3 的帧头通常只有 2 字节(类型 1 + 长度 1)。
握手
1-RTT(首次连接)
TCP + TLS 1.3:
Client ──── SYN ──────────→ ┐
Client ←─── SYN-ACK ─────── │ 1 RTT (TCP)
Client ──── ACK + TLS CH ─→ ┘
Client ←─── TLS SH+FIN ──── ┐ 1 RTT (TLS)
Client ──── TLS FIN + 数据 → ┘
= 2 RTT 才能发第一个字节
QUIC:
Client ──── Initial(CH) ──→ ┐
Client ←─── Initial(SH)+Handshake ─ │ 1 RTT
Client ──── Handshake + 数据 ──────→ ┘
= 1 RTTQUIC 把传输握手与 TLS 握手合并成一次——加密不是叠在传输之上,而是内建其中。
0-RTT(会话恢复)
客户端持有上次连接的会话票据时,可在第一个包里就带应用数据:
Client ──── Initial + 0-RTT 数据 ──→ ← 零等待重放风险:0-RTT 数据没有前向的握手确认,攻击者截获后可重放。因此:
- 服务端 MUST NOT 在 0-RTT 中处理非幂等请求,或必须有去重机制。
- 拒绝时返回
425 Too Early,客户端在握手完成后重发。 - 客户端 SHOULD 只在 0-RTT 中发送安全且幂等的请求(
GET/HEAD)。
连接 ID 与连接迁移
TCP 连接由四元组标识(源 IP、源端口、目的 IP、目的端口)。手机从 WiFi 切到 4G,IP 变了 → 四元组变了 → 连接必断,所有在途请求失败、需重新握手。
QUIC 改用 Connection ID(CID):
- 每个包携带 CID,接收方靠 CID 而非四元组查找连接。
- 客户端换网络后,用同一个 CID 从新地址继续发包,连接无缝延续——TLS 会话、拥塞状态、在途流全部保留。
- 服务端需通过路径验证(发送随机数据要求对方回应)确认新地址可达,防止地址欺骗与放大攻击。
- 为防跨路径追踪,双方会预先交换一组备用 CID,迁移时同时更换 CID,使旁观者无法把新旧路径关联起来。
这是移动场景下 h3 最直观的用户可感收益。
流量控制
QUIC 自带两级流量控制,h2 的那一层因此被删除:
| 级别 | 帧 | 传输参数(初始值) |
|---|---|---|
| 流级 | MAX_STREAM_DATA | initial_max_stream_data_bidi_local/remote |
| 连接级 | MAX_DATA | initial_max_data |
| 流数量 | MAX_STREAMS | initial_max_streams_bidi/uni |
与 h2 的 WINDOW_UPDATE 的区别:QUIC 的帧传的是新的绝对上限,而非增量——重传时天然幂等,不会因重复计算而错乱。
加密范围
QUIC 包结构:
┌──────────────────────────────────────┐
│ 首字节 + 版本 + CID (明文,供路由) │ ← 中间件只能看到这些
├──────────────────────────────────────┤
│ 包号 (头部保护加密) │
├──────────────────────────────────────┤
│ 载荷: 全部 QUIC 帧 (AEAD 加密) │ ← ACK、流数据、流控全在这里
└──────────────────────────────────────┘对照 TCP:序号、ACK、窗口、选项全部明文,中间件可读可改。QUIC 把它们全加密,带来:
- 抗僵化:中间件无法依赖内部结构,协议才能持续演进。
- 抗窥探:观察者无法统计单个流的行为。
- 代价:运维失去了传统抓包分析能力,需改用 qlog(端点导出的结构化日志)排障。
丢包恢复的改进
- 包号单调递增,重传用新包号——消除了 TCP 的「重传歧义」(收到 ACK 时无法区分确认的是原包还是重传包),RTT 估算因此更准。
- ACK 帧支持多达 256 个 ACK Range,比 TCP SACK 的 3–4 个 block 精确得多,能更快识别真实丢失。
- 拥塞控制在用户态,可按需换算法(默认类似 NewReno,普遍部署 BBR)。
下一篇:HTTP/3 帧格式。