QUIC 传输层

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

作者 David更新于 第 11 篇(共 16 篇)

RFC 9000(传输)、9001(TLS)、9002(丢包检测与拥塞控制)。这一篇只讲 HTTP/3 用得到的部分。

流

QUIC 原生提供多条独立的有序字节流,这是它与 TCP 的根本差异:

TCPQUIC
抽象一条有序字节流多条独立有序流
某段丢失时全部后续数据阻塞只阻塞该流
流量控制连接级流级 + 连接级
流的创建成本一次握手零(发第一个字节即创建)

关键: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 位总字节数可表示范围
0010 – 63
0120 – 16383
1040 – 2^30−1
1180 – 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 RTT

QUIC 把传输握手与 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_DATAinitial_max_stream_data_bidi_local/remote
连接级MAX_DATAinitial_max_data
流数量MAX_STREAMSinitial_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 帧格式。

本页目录