# QUIC 传输层

> 流与流 ID、变长整数、1-RTT/0-RTT 握手、连接 ID 与连接迁移。

- 作者：David（道雾轩）
- 专栏：HTTP/2 与 HTTP/3 协议详解（https://daiw.org/manual/http2-3.md）
- 最后更新：2026-07-26
- 原文：https://daiw.org/manual/http2-3/quic-transport
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

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 规则](https://daiw.org/manual/http2-3/h2-streams)（奇偶分客户端/服务端）：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 字节帧头](https://daiw.org/manual/http2-3/h2-framing)——h3 的帧头通常只有 **2 字节**（类型 1 + 长度 1）。

## 握手

### 1-RTT（首次连接）

```text
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 RTT
```

QUIC 把传输握手与 TLS 握手**合并**成一次——加密不是叠在传输之上，而是**内建**其中。

### 0-RTT（会话恢复）

客户端持有上次连接的会话票据时，可在**第一个包里就带应用数据**：

```text
  Client ──── Initial + 0-RTT 数据 ──→   ← 零等待
```

**重放风险**：0-RTT 数据没有前向的握手确认，攻击者截获后可重放。因此：

- 服务端 **MUST NOT** 在 0-RTT 中处理**非幂等**请求，或必须有去重机制。
- 拒绝时返回 [`425 Too Early`](https://daiw.org/manual/http11/status-4xx)，客户端在握手完成后重发。
- 客户端 **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 的那一层因此被删除](https://daiw.org/manual/http2-3/h2-flow-control)：

| 级别 | 帧 | 传输参数（初始值） |
| --- | --- | --- |
| 流级 | `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 的帧传的是**新的绝对上限**，而非增量——重传时天然幂等，不会因重复计算而错乱。

## 加密范围

```text
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 帧格式](https://daiw.org/manual/http2-3/h3-framing)。
