阅读路径(TLS 1.3 ↔︎ QUIC ↔︎ HTTP/3)
文章 分工 TLS 1.3 工程实践 RFC 8446 部署:1-RTT/0-RTT、 early_data重放风险TLS 1.3 最小实现 握手与 HKDF 源码级拆解(与工程文二选一打底) QUIC 上 设计动机;RFC 9000 传输语义 QUIC 下 Initial→1-RTT 实现;RFC 9001 TLS 映射 本文 RFC 9114 应用层:QPACK、流映射、H3 协商(Alt-Svc) 建议顺序:TLS 1.3(工程或 crypt)→ QUIC 上/下 → 本文。下文假定已理解 QUIC 流、Connection ID 与 TLS 1.3 内嵌握手。
QUIC 上 与 QUIC 下 把传输层拆完:丢包不阻塞单流、连接可迁移、握手可合并为 1-RTT(RFC 9000, Section 7;RFC 9001, Section 4)。加密语义与 TLS 1.3 工程实践 / 最小实现 同源。
但问题来了:HTTP 怎么跑在 QUIC 上?
直觉上最简单的方案是把 HTTP/2 原封不动搬到 QUIC 上。实际上 Google 最早的 gQUIC 就是这么干的,结果发现一堆问题——HTTP/2 的很多设计是为了弥补 TCP 的缺陷,而 QUIC 已经解决了这些缺陷,再叠一层就成了冗余。
所以 IETF 的做法是:在 QUIC 之上重新设计应用层协议,这就是 HTTP/3(RFC 9114)。帧格式变了(RFC 9114, Section 7),头部压缩换成 QPACK(RFC 9204),HTTP 层流控删掉——由 QUIC 承担(RFC 9114, Section 4.1)。看起来变化很大,但职责划分反而更清晰。
这篇就把 HTTP/3 从里到外拆一遍。
一、HTTP/3 和 HTTP/2 的本质区别
先上结论,再展开。
| 维度 | HTTP/2 | HTTP/3 |
|---|---|---|
| 传输层 | TCP + TLS 1.2/1.3 | QUIC(内含 TLS 1.3) |
| 多路复用 | HTTP 层模拟的流(stream ID) | 直接映射到 QUIC stream |
| 流控 | HTTP/2 自己做(WINDOW_UPDATE) | 删掉,用 QUIC 的流控 |
| 头部压缩 | HPACK | QPACK |
| 队头阻塞 | 有(TCP 层) | 单流内无;连接仍共享 CC(RFC 9002) |
| 连接迁移 | 不支持 | 支持(Connection ID) |
| 帧格式 | 固定 9 字节头 | 变长编码 |
HTTP/2:在一条 TCP 连接上模拟多路复用
HTTP/2 的核心卖点是多路复用——在一条 TCP 连接上同时跑多个请求。它用 stream ID 来区分不同的请求,用 HEADERS 帧和 DATA 帧来承载数据。
但问题是,TCP 是一个有序字节流。所有的 HTTP/2 帧最终都要排队走同一条 TCP 连接。如果前面的 TCP 包丢了,后面所有帧都得等——哪怕它们属于完全不同的 HTTP 请求。
HTTP/2 over TCP:
Stream 1: HEADERS ──── DATA ──── DATA
Stream 3: HEADERS ──── DATA ← 全部排队走同一条 TCP 连接
Stream 5: HEADERS ──── DATA ──── DATA
│ │ │
▼ ▼ ▼
┌──────────────────────────────────┐
│ 一条 TCP 连接 │ ← 包 #3 丢了?全部等!
└──────────────────────────────────┘
这就是臭名昭著的 TCP 层队头阻塞(Head-of-Line Blocking)。
HTTP/3:一个请求 = 一个 QUIC stream
HTTP/3 的做法(RFC 9114, Section 6):每个 HTTP 请求/响应交换映射到一条 QUIC 双向流。QUIC stream 之间独立——stream 3 丢包重传时,stream 5 与 stream 7 仍可继续(传输层 HoL 消除见 RFC 9000, Section 2.2)。注意:单条 QUIC 连接内仍共享拥塞控制(RFC 9002),「应用层无 HoL」不等于「连接级带宽无限」。
HTTP/3 over QUIC:
QUIC Stream 0: HEADERS ── DATA ── DATA (请求 1)
QUIC Stream 4: HEADERS ── DATA (请求 2)
QUIC Stream 8: HEADERS ── DATA ── DATA (请求 3)
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 独立传输 │ │ 独立传输 │ │ 独立传输 │ ← 互不干扰!
└──────────┘ └──────────┘ └──────────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────┐
│ 一条 QUIC 连接(UDP) │
└──────────────────────────────────────┘
(完整的流映射关系可以看下方的 HTTP/3 Stream Mapping 图)
删掉冗余:不再需要 HTTP 层流控
HTTP/2 有自己的一套流控机制——WINDOW_UPDATE
帧告诉对方「我还能接收多少数据」。这是因为 TCP
的流控粒度是整条连接,HTTP/2 需要更细粒度的按流控制。
但 QUIC 天生就提供了按 stream 的流控。所以 HTTP/3 直接删掉了 HTTP 层的流控——没必要做两遍。
同样被删掉的还有 HTTP/2 的 PRIORITY
帧。HTTP/3 改用 Extensible Priorities(RFC
9218),通过 priority
头字段表达优先级——浏览器与 CDN
的实际调度效果仍在演进,不宜假设与 HTTP/2 优先级树等价。
二、QPACK:HTTP/3 的头部压缩
头部压缩是 HTTP 性能优化的重要一环。一个典型的 HTTP
请求可能有 300-800
字节的头部,其中大量是重复的(:method: GET、:scheme: https、user-agent: ...)。
HPACK 的问题
HTTP/2 用的 HPACK 方案很聪明:维护一张动态表,把见过的头部存起来,后续只发索引号。问题是——动态表的更新依赖有序传递。
HPACK 动态表更新:
编码端:
发送请求 1 → 往动态表插入 "custom-header: value1" → 索引 62
发送请求 2 → 引用索引 62 → 只发 1 字节
解码端:
必须先收到请求 1(知道索引 62 是什么)
才能解码请求 2
在 TCP 上没问题,因为 TCP 保证有序。但 QUIC 的 stream 之间是无序的——请求 2 可能先到,这时候解码端不知道索引 62 是什么,直接就炸了。
QPACK 的解法
QPACK(RFC 9204)把动态表更新和数据传输解耦。它引入两条专门的单向流(RFC 9204, Section 4.2):
- Encoder Stream:编码端 → 解码端,传递动态表更新指令
- Decoder Stream:解码端 → 编码端,确认动态表更新已收到
QPACK 架构:
┌────────────────────────────────────────────────┐
│ QUIC 连接 │
│ │
│ Encoder Stream (单向) ─────────────────────► │
│ 「索引 0 = custom-header: value1」 │
│ 「索引 1 = another-header: value2」 │
│ │
│ Decoder Stream (单向) ◄───────────────────── │
│ 「确认:已处理到索引 1」 │
│ │
│ Request Stream 0 → HEADERS(引用索引 0, 1) │
│ Request Stream 4 → HEADERS(引用索引 0) │
│ Request Stream 8 → HEADERS(引用静态表) │
└────────────────────────────────────────────────┘
关键设计(RFC
9204, Section 4.4):若 HEADERS 引用的动态表条目尚未经
Encoder Stream 同步,该 stream
阻塞等待(QPACK_BLOCKED_STREAMS
上限见 RFC
9114, Section 7.2.4),而非直接解码失败;其他 stream
不受影响。
这样就把「全局有序」降级成了「按需等待」,保住了 QUIC 的无序特性。
静态表:98 个预定义头
QPACK 预定义 98 个常用头部(RFC 9204, Appendix A;HPACK 静态表见 RFC 7541, Appendix B):
# QPACK 静态表(部分)
索引 0: :authority
索引 1: :path /
索引 2: age 0
索引 3: content-disposition
索引 4: content-length 0
索引 5: cookie
...
索引 15: :method CONNECT
索引 16: :method DELETE
索引 17: :method GET
索引 18: :method HEAD
索引 19: :method OPTIONS
索引 20: :method POST
索引 21: :method PUT
...
索引 24: :scheme http
索引 25: :scheme https
索引 26: :status 103
索引 27: :status 200
索引 28: :status 304
索引 29: :status 404
索引 30: :status 503
...
索引 96: x-frame-options deny
索引 97: x-frame-options sameorigin
编码效率对比(机制层,非 benchmark)
| 指标 | HPACK (HTTP/2) | QPACK (HTTP/3) |
|---|---|---|
| 静态表大小 | 61 条(RFC 7541) | 98 条(RFC 9204) |
| 动态表同步 | 依赖 TCP 有序 | Encoder/Decoder Stream |
| Huffman 编码 | 支持 | 支持(相同码表,RFC 9204 Section 4.3) |
| 乱序容忍 | 否 | 是(单流可阻塞) |
| 实现复杂度 | 中等 | 较高(需管理两条控制流 + blocked stream) |
压缩率随站点头部分布变化极大;不宜引用未标注出处的「首次 ~60% / 后续 ~90%」类百分比——对比应在本机 trace 上自测,或引用带 workload 说明的第三方报告。
三、HTTP/3 帧格式
HTTP/2 的帧有一个固定的 9 字节头部:
HTTP/2 帧格式(固定 9 字节头):
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length (24) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type (8) | Flags (8) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|R| Stream Identifier (31) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Payload ... |
HTTP/3 简化了很多——因为 stream ID 和流控都由 QUIC 管了,帧头只需要类型和长度:
HTTP/3 帧格式(变长编码):
┌───────────────────────────────────┐
│ Type (变长整数) │
├───────────────────────────────────┤
│ Length (变长整数) │
├───────────────────────────────────┤
│ Payload ... │
└───────────────────────────────────┘
变长整数编码(QUIC Variable-Length Integer)用前 2 bit
表示长度:00 = 1 字节,01 = 2
字节,10 = 4 字节,11 = 8
字节。大多数帧的类型和长度只需要 1-2 字节。
HTTP/3 的帧类型
| 帧类型 | Type 值 | 用途 | HTTP/2 对应 |
|---|---|---|---|
| DATA | 0x00 | 请求/响应的 body | DATA |
| HEADERS | 0x01 | 头部(QPACK 编码) | HEADERS |
| CANCEL_PUSH | 0x03 | 取消一个 Server Push | — |
| SETTINGS | 0x04 | 连接配置参数 | SETTINGS |
| PUSH_PROMISE | 0x05 | Server Push 承诺 | PUSH_PROMISE |
| GOAWAY | 0x07 | 优雅关闭连接 | GOAWAY |
| MAX_PUSH_ID | 0x0d | 允许的最大 Push ID | — |
注意几个被删掉的帧类型:
- WINDOW_UPDATE:QUIC 自带流控,不需要了
- PRIORITY:换成 Extensible
Priorities(HTTP 头字段
priority) - RST_STREAM:用 QUIC 的
RESET_STREAM帧代替 - PING:用 QUIC 的
PING帧代替 - CONTINUATION:HTTP/3 允许 HEADERS 帧跨越多个 QUIC 包,不再需要
控制流(Control Stream)
HTTP/3
有一条特殊的单向流叫控制流。连接建立后,双方各打开一条控制流,用来交换
SETTINGS、GOAWAY
等连接级别的帧。
控制流的作用:
客户端 ──── Control Stream ────► 服务端
发送 SETTINGS: {QPACK_MAX_TABLE_CAPACITY=4096, ...}
发送 GOAWAY (准备关闭时)
服务端 ──── Control Stream ────► 客户端
发送 SETTINGS: {MAX_PUSH_ID=0, ...}
发送 GOAWAY (准备关闭时)
控制流的 stream type 是
0x00,必须是连接建立后最先打开的单向流。
Server Push 在 HTTP/3 中的变化
说实话,Server Push 在实践中几乎没人用——Chrome 在 2022 年就默认禁用了 HTTP/2 Server Push。HTTP/3 保留了这个功能但做了简化:
- 服务端在请求流上发送
PUSH_PROMISE帧(而不是 HTTP/2 的 promised stream) - 推送的响应在一条新的单向推送流上发送
MAX_PUSH_ID帧用来限制推送数量CANCEL_PUSH帧用来取消推送
现实中建议直接设 MAX_PUSH_ID=0,即不使用
Server Push。
四、完整请求链路拆解
现在把所有零件组装起来。一个完整的 HTTP/3 请求从建立连接到收到响应,分 5 步。
步骤 1:QUIC 握手(1-RTT 或 0-RTT)
客户端发送 QUIC Initial 包,内含 TLS ClientHello。1-RTT 握手完成后,双方共享加密密钥,QUIC 连接建立。
如果之前连接过,客户端可能有缓存的 session ticket,可以做 0-RTT 握手——在第一个包里就带上应用数据。
(具体握手细节见 QUIC 协议拆解(上):为什么 TCP 改不动了)
步骤 2:交换 SETTINGS 帧
QUIC
连接建立后,双方立刻各开一条单向控制流,发送
SETTINGS 帧:
# SETTINGS 帧的典型参数
SETTINGS {
QPACK_MAX_TABLE_CAPACITY = 4096 # QPACK 动态表最大字节数
QPACK_BLOCKED_STREAMS = 100 # 允许被 QPACK 阻塞的最大流数
MAX_FIELD_SECTION_SIZE = 8192 # 单个头部块的最大字节数
}
同时,双方各开一条 QPACK Encoder Stream 和 Decoder Stream。此时连接上已经有 6 条单向流:
单向流布局:
客户端 → 服务端:
Stream Type 0x00: Control Stream (SETTINGS, GOAWAY)
Stream Type 0x02: QPACK Encoder (动态表更新)
Stream Type 0x03: QPACK Decoder (确认收到)
服务端 → 客户端:
Stream Type 0x00: Control Stream
Stream Type 0x02: QPACK Encoder
Stream Type 0x03: QPACK Decoder
步骤 3:客户端打开 QUIC stream,发送 HEADERS 帧
客户端打开一条双向 QUIC stream(stream ID = 0),发送 HEADERS 帧:
# 请求 GET https://example.com/api/data
HEADERS Frame on QUIC Stream 0:
┌──────────────────────────────────────┐
│ Frame Type: 0x01 (HEADERS) │
│ Frame Length: 47 │
│ QPACK Encoded Header Block: │
│ Required Insert Count: 0 │
│ Base: 0 │
│ :method = GET (静态表 17) │
│ :scheme = https (静态表 25) │
│ :authority = example.com (字面量) │
│ :path = /api/data (字面量) │
│ accept = application/json (字面量) │
└──────────────────────────────────────┘
注意 Required Insert Count 字段——它告诉解码端:「解码这个头部块需要动态表至少有多少条目」。如果是 0,说明只用了静态表和字面量,不需要等 Encoder Stream。
步骤 4:服务端回复 HEADERS + DATA 帧
服务端在同一条 QUIC stream上回复:
# 服务端响应
HEADERS Frame on QUIC Stream 0:
┌──────────────────────────────────────┐
│ Frame Type: 0x01 (HEADERS) │
│ QPACK Encoded Header Block: │
│ :status = 200 (静态表 27) │
│ content-type = application/json │
│ content-length = 1234 │
└──────────────────────────────────────┘
DATA Frame on QUIC Stream 0:
┌──────────────────────────────────────┐
│ Frame Type: 0x00 (DATA) │
│ Frame Length: 1234 │
│ Payload: {"users": [...]} │
└──────────────────────────────────────┘
步骤 5:流关闭
服务端发完数据后,发送 QUIC 的 FIN
位,表示这条 stream
的发送方向关闭。客户端收到完整响应后,也关闭自己的发送方向。这条双向
stream 就完成了它的使命。
如果客户端想发第二个请求,它会打开一条新的 QUIC stream(stream ID = 4),完全独立。
用 curl 实际抓包
curl 从 7.66 开始支持 HTTP/3(需要编译时启用)。用
--http3 选项可以强制使用 HTTP/3:
# 用 curl 发起 HTTP/3 请求
curl --http3 -v https://cloudflare-quic.com/
# 输出(关键部分):
# * using HTTP/3
# * [QUIC] Connected to cloudflare-quic.com:443
# * [QUIC] Using QUIC v1 (RFC 9000)
# * h3 [stream 0] [HEADERS]
# * :status: 200
# * content-type: text/html
# * alt-svc: h3=":443"; ma=86400
# * [stream 0] [DATA] 15234 bytes用 Wireshark 抓包可以看到 QUIC 层的细节:
# 用 tshark 抓 QUIC 包
tshark -i eth0 -f "udp port 443" \
-o "tls.keylog_file:keylog.txt" \
-Y "quic" \
-T fields -e quic.stream_id -e http3.frame_type
# 配合 SSLKEYLOGFILE 环境变量导出密钥
SSLKEYLOGFILE=keylog.txt curl --http3 https://example.com/在 Wireshark 中你会看到: 1. QUIC Initial(TLS ClientHello) 2. QUIC Handshake(TLS ServerHello + 证书) 3. QUIC 1-RTT(SETTINGS、HEADERS、DATA)
五、性能:机制收益与引用数据
本节不包含本站自测 QPS/延迟表。
可核对的部分来自 RFC 定义的 RTT 步数;端到端吞吐、TTFB、P99
随页面大小、CDN 拓扑、丢包与 CPU offload
能力变化——下文只写机制推论与带来源的外部报告,复现请用文末
curl + tc netem 在本环境自测。
5.1 握手 RTT(规范层)
HTTP/2 (TCP + TLS 1.3):
Client ──── SYN ────────────► Server ┐
Client ◄─── SYN-ACK ─────── Server │ 1 RTT (TCP)
Client ──── ACK + ClientHello ► Server │
Client ◄─── ServerHello ──── Server │ 1 RTT (TLS)
Client ──── Finished ───────► Server ┘
总计: 2 RTT([RFC 8446](https://www.rfc-editor.org/rfc/rfc8446) + TCP 三次握手)
HTTP/3 (QUIC + TLS 1.3):
Client ──── Initial(ClientHello) ► Server ┐
Client ◄─── Initial(ServerHello) ─ Server │ 1 RTT
Client ──── Handshake + 请求 ────► Server ┘
总计: 1 RTT([RFC 9001, Section 4](https://www.rfc-editor.org/rfc/rfc9001#section-4))
HTTP/3 (0-RTT 恢复):
Client ──── Initial + 0-RTT 数据 ► Server ┐
Client ◄─── 响应 ──────────────── Server │ 0 RTT(应用数据角度)
总计: 0 RTT([RFC 8446, Section 2.3](https://www.rfc-editor.org/rfc/rfc8446#section-2.3);[RFC 9001, Section 4.6](https://www.rfc-editor.org/rfc/rfc9001#section-4.6))
冷启动节省 1 RTT 在跨洋链路上可直接换算为毫秒级延迟差;高带宽、短连接场景下这一差值往往小于连接建立后的传输时间,因此「HTTP/3 一定更快」不成立——需按业务 trace 测量。
5.2 多路复用、丢包与尾延迟
机制:HTTP/2 所有帧共享一条 TCP 字节流,丢包触发 TCP 重传时会阻塞同连接上全部 stream(RFC 9113 over TCP 的固有限制)。HTTP/3 把请求映射到独立 QUIC stream(RFC 9114, Section 6),丢包重传粒度在 stream 级(RFC 9000, Section 2.2)。
外部引用(Google 生产部署,非通用 benchmark):Langley et al., The QUIC Transport Protocol: Design and Internet-Scale Deployment, SIGCOMM 2017 报告,在 Google 搜索与 YouTube 客户端上 QUIC 相对 TCP+TLS 的延迟与卡顿率有个位数到两位数百分比量级的改善(具体数值依赖 Google 内部 workload 与客户端版本,见该文 Section 6 实测)。公网长期观测见 Kakhki et al., Taking a Long Look at QUIC, IMC 2017——结论之一是 QUIC 收益在高丢包、移动网络子集上更明显,干净链路差距缩小。
工程含义:若你的用户主要在低丢包数据中心或企业内网,HTTP/3 相对 HTTP/2 的吞吐优势可能不显著;若面向移动网、跨境 Wi‑Fi,更值得投入 UDP 443 可达性与 Alt-Svc 发现链路。
5.3 连接迁移
IP 变更时 HTTP/2 必须重建 TCP+TLS;QUIC 用 Connection ID 维持连接(RFC 9000, Section 9),配合 0-RTT 可跳过完整握手。原理见 QUIC 下。恢复时间取决于路径 RTT、ticket 缓存与服务器 Early-Data 策略(TLS 1.3 工程实践 第五节),不宜写死「× 秒」类数字。
5.4 CPU 与用户态栈
QUIC 加密、ACK、CC 多在用户态完成(RFC
9000),相对内核 TCP + kTLS/offload 路径 CPU
开销更高——Langley et al. (SIGCOMM 2017) 在 Google 边缘曾报告
QUIC 相对 TCP 的 CPU 成本上升,并通过批量发送(如
GSO/sendmmsg)缓解。Linux 6.x 起内核 QUIC
模块与 DPDK/XDP
等路径仍在演进,边缘节点容量规划必须按本机 QPS
压测,不能套用未标注硬件的倍数。
QPACK 编解码比 HPACK 多两条控制流(RFC 9204),在头部极大或动态表频繁更新的 API 上可能放大 CPU 占比——同样需 profile 验证。
5.5 本地对比方法(需自测)
# 模拟丢包/延迟后对比 time_total(结果仅对本机、本站点有效)
sudo tc qdisc add dev eth0 root netem loss 2% delay 50ms
curl -w "h2 time_total: %{time_total}s\n" --http2 -o /dev/null -s https://example.com/
curl -w "h3 time_total: %{time_total}s\n" --http3 -o /dev/null -s https://example.com/
sudo tc qdisc del dev eth0 root并发、页面大小、CDN 缓存状态都会改变结论;不要把单次 curl 输出写成普适性能排名。
六、部署 HTTP/3 的实际问题
理论讲完了,现在说说真上线要踩的坑。
Web 服务器支持现状
| 服务器 | HTTP/3 支持 | QUIC 库 | 备注 |
|---|---|---|---|
| Nginx | 1.25.0+ 原生支持 | quictls / BoringSSL | 需编译时
--with-http_v3_module |
| Caddy | 默认开启 | quic-go (Go) | 零配置,开箱即用 |
| H2O | 完整支持 | quicly (C) | 性能最好,但用户少 |
| LiteSpeed | 完整支持 | lsquic (C) | 商业产品,性能强 |
| Apache | 实验性 | — | 不推荐生产使用 |
Caddy 是最简单的选择——安装即支持 HTTP/3,不需要额外配置:
# Caddy 配置 - 自动支持 HTTP/3
example.com {
# 自动获取 TLS 证书
# 自动启用 HTTP/3
# 自动添加 Alt-Svc 头
root * /var/www/html
file_server
}
Nginx 需要手动编译和配置:
# Nginx HTTP/3 配置
server {
# 同时监听 TCP (HTTP/2) 和 UDP (HTTP/3)
listen 443 ssl;
listen 443 quic reuseport;
server_name example.com;
ssl_certificate /etc/ssl/certs/example.com.pem;
ssl_certificate_key /etc/ssl/private/example.com.key;
# 告诉浏览器可以用 HTTP/3
add_header Alt-Svc 'h3=":443"; ma=86400';
# 开启 0-RTT(注意重放攻击风险)
ssl_early_data on;
location / {
root /var/www/html;
}
}
Alt-Svc:HTTP/3(H3)的协商与发现机制
浏览器默认不直接猜 UDP 443——先用 TCP 建连,服务端在响应头声明 HTTP/3 能力,完成从 HTTP/2 到 H3 的协商发现(RFC 9114, Section 3.1;Alt-Svc 语法见 RFC 7838):
# 服务端响应头
HTTP/2 200 OK
alt-svc: h3=":443"; ma=86400
content-type: text/html
# 解读:
# h3=":443" → 支持 HTTP/3,端口 443
# ma=86400 → 这个信息有效期 24 小时
浏览器收到后,下次请求会同时尝试 TCP 和 UDP(称为 Happy Eyeballs v2),哪个先连上用哪个。一旦 HTTP/3 连接成功,后续请求就全走 HTTP/3。
还有一种更快的方式——HTTPS RR(RFC 9460):
# DNS 查询返回
example.com. IN HTTPS 1 . alpn="h3,h2" port=443
# 浏览器看到 alpn="h3" 后,可以直接尝试 HTTP/3
# 不需要先走 HTTP/2 再升级
企业防火墙阻断 UDP:深水区
这是 HTTP/3 部署最大的现实障碍。很多企业防火墙默认放行 TCP 443,但过滤 UDP 443。
引用数据:Kakhki et al. (IMC 2017)
对公网 QUIC
可达性做过长期测量,显示中间盒与路径策略会导致部分客户端无法完成
QUIC 握手;Cloudflare、Google 边缘博客亦多次讨论企业网 UDP
过滤——具体失败比例因地区与 ASN
而异,本站未复现,上线应监控 h3_fallback_to_h2
类指标。
QUIC-over-TCP 回退
当 UDP 完全不通时,QUIC 协议本身跑不起来。社区讨论过 QUIC-over-TCP 封装(类似 WireGuard over TCP 的思路),但这基本抵消了 QUIC 的所有优势——你在 TCP 上跑 QUIC,又在 QUIC 上跑 HTTP/3,多了一层封装,队头阻塞又回来了。
RFC 9369(QUIC Version 2)只更新版本协商与加密套件,不提供 QUIC-over-TCP 标准化回退(RFC 9369)。UDP 不通时实践上回退 HTTP/2 over TCP。
Alt-Svc 升级机制
浏览器不会盲目尝试 HTTP/3。升级流程是渐进式的:
首次访问 example.com:
1. 浏览器用 TCP + TLS 建立 HTTP/2 连接(稳妥的默认行为)
2. 服务端在响应头中声明:Alt-Svc: h3=":443"; ma=86400
3. 浏览器缓存这个信息(有效期 24 小时)
第二次访问 example.com:
4. 浏览器发现缓存中有 Alt-Svc 记录
5. 同时发起 TCP 和 UDP 连接(Happy Eyeballs)
6. 哪个先成功用哪个
7. 如果 HTTP/3 成功,后续请求全走 HTTP/3
更快的方式是用 HTTPS DNS 记录(RFC 9460),在 DNS 解析阶段就告知客户端支持 HTTP/3:
example.com. IN HTTPS 1 . alpn="h3,h2" port=443
这样浏览器在首次访问时就能尝试 HTTP/3,不需要先走 HTTP/2 “发现” Alt-Svc。
Happy Eyeballs:TCP + QUIC 并行
RFC 8305 解决地址族选择;Chromium 等实现将其扩展到「QUIC 与 TCP 赛跑」——典型行为是优先尝试 QUIC,若在实现定义的超时内无响应则并行发起 TCP(具体毫秒数随版本变化,以浏览器源码为准,不宜写死为 universal 常量)。QUIC 多次失败后会对域名临时禁用 HTTP/3 并指数退避重试。
企业中间件兼容策略
对于必须穿越企业防火墙/代理的场景:
- 申请开放 UDP 443:最直接的方案。QUIC 用的是 UDP 443 端口,和 HTTPS 相同端口号,安全策略上容易审批
- 部署在防火墙前面:CDN 边缘节点天然在防火墙外侧,Cloudflare/Akamai 已全面支持 HTTP/3
- 检测并降级:客户端实现 QUIC 可用性探测,不通则降级到 HTTP/2,记录降级比例用于推动网络改造
- 透明代理兼容:一些中间件会篡改 UDP 包导致 QUIC 握手失败——QUIC 的 Connection ID 和加密设计让中间件无法有效代理,这既是安全优势也是兼容性障碍
应对策略:
# 客户端(浏览器)的降级逻辑伪代码
def connect(url):
# 1. 如果有缓存的 Alt-Svc 信息,先尝试 HTTP/3
if has_alt_svc_cache(url):
h3_conn = try_quic_connect(url, timeout=300ms)
if h3_conn:
return h3_conn
# QUIC 失败,标记这个域名暂时不用 HTTP/3
mark_quic_broken(url, duration=5min)
# 2. 回退到 HTTP/2 over TCP
h2_conn = tcp_connect(url)
# 3. 检查 Alt-Svc 头,为下次请求做准备
if 'alt-svc' in h2_conn.response_headers:
cache_alt_svc(url, h2_conn.response_headers['alt-svc'])
return h2_connChromium 等实现会在 QUIC 反复失败后对域名启用临时 QUIC 禁用与指数退避(具体时长为实现细节,以目标浏览器版本为准)。
QUIC 上的拥塞控制
HTTP/3 继承 QUIC 连接级 CC(RFC 9002),在用户态实现——迭代算法不需等内核发版,但同一瓶颈链路上可能与 TCP 竞争带宽。
为什么用户态 CC 是优势
TCP CC 在内核全局生效;QUIC 栈可在进程内切换 NewReno、CUBIC、BBR 等(RFC 9002, Appendix B 列举参考算法)。Google 在 SIGCOMM 2017 文中描述了生产环境快速灰度 CC 的能力。
为什么用户态 CC 是风险
多 QUIC 流与 TCP 流共享瓶颈时,若 QUIC 侧 CC 更激进,可能不公平抢占带宽——Cardwell et al., BBR: Congestion-Based Congestion Control, ACM Queue 2016 及后续 BBRv2 工作讨论了与 CUBIC TCP 共存时的公平性问题;RFC 9002, Section 7 亦要求实现文档说明与 TCP 的互操作考量。
本站不提供 CC 算法吞吐排名表——不同丢包率、缓冲、bulk 与 web 短流混合下结论相反。部署建议(工程判断,非 benchmark):
- 公网混合流量:默认 CUBIC 或与 TCP 公平性经过验证的 BBRv2 配置
- 可控 DC / CDN 边缘:可按 trace 压测 BBR 族;高丢包卫星/移动链路优先验证重传与 CC 联动
- 与 HTTP/3 收益解耦:高丢包下 HTTP/3 的主要增益来自 stream 级 HoL 消除,不应全部归因于 CC 选型
监控和调试工具
上线 HTTP/3 后,你需要能看到它:
# 1. curl 检查 HTTP/3 支持
curl -sI --http3 https://example.com 2>&1 | grep -i "http/3\|alt-svc"
# 2. 浏览器开发者工具
# Network 面板 → Protocol 列 → 显示 "h3"
# 3. Wireshark 解析 QUIC
# 过滤器: quic && http3
# 需要配置 TLS key log 才能解密
# 4. qlog:QUIC 专用日志格式
# https://github.com/quicwg/qlog
# 用 qvis (https://qvis.quictools.info/) 可视化
# 5. 服务端指标(Prometheus 示例)
# quic_connections_active 当前活跃 QUIC 连接数
# quic_handshake_duration_ms 握手延迟分布
# quic_streams_opened_total 打开的 stream 总数
# quic_packet_loss_rate 丢包率
# h3_requests_total HTTP/3 请求总数
# h3_fallback_to_h2_total 降级到 HTTP/2 的次数一个实用的健康检查脚本:
#!/bin/bash
# 检查目标站点的 HTTP/3 支持状态
TARGET="https://example.com"
echo "=== HTTP/3 健康检查 ==="
# 检查 Alt-Svc 头
ALT_SVC=$(curl -sI "$TARGET" 2>/dev/null | grep -i "alt-svc")
if [ -n "$ALT_SVC" ]; then
echo "[是] Alt-Svc 头存在: $ALT_SVC"
else
echo "[否] 未发现 Alt-Svc 头"
fi
# 尝试 HTTP/3 连接
H3_STATUS=$(curl -s -o /dev/null -w "%{http_version}" --http3 "$TARGET" 2>/dev/null)
if [ "$H3_STATUS" = "3" ]; then
echo "[是] HTTP/3 连接成功"
else
echo "[否] HTTP/3 连接失败(可能是 UDP 被阻断)"
fi
# 对比延迟
H2_TIME=$(curl -s -o /dev/null -w "%{time_total}" --http2 "$TARGET" 2>/dev/null)
H3_TIME=$(curl -s -o /dev/null -w "%{time_total}" --http3 "$TARGET" 2>/dev/null)
echo "HTTP/2 延迟: ${H2_TIME}s"
echo "HTTP/3 延迟: ${H3_TIME}s"七、开放问题
- QPACK blocked stream 与优先级:动态表同步阻塞单 stream(RFC 9204, Section 4.4)与 RFC 9218 优先级调度如何协同、是否引入新的尾延迟——浏览器与 CDN 实现分歧大,缺统一基准。
- 发现路径:Alt-Svc(RFC 7838)与 HTTPS RR(RFC 9460)并存,首次访问仍常需 TCP 探路;「零 RTT 发现 HTTP/3」尚无单一 IETF 强制 profile。
- 0-RTT + HTTP 语义:TLS Early Data(RFC 8446, Section 8)与 HTTP 幂等/重放边界在 API 网关层仍靠各厂自行约束——与 TLS 1.3 工程实践 第七节同源未决问题。
- QUIC–TCP 公平性与运营商策略:用户态 CC(RFC 9002)在混合瓶颈上的公平性无跨实现标准答案;部分网络对 UDP 443 限速/过滤的原因与趋势仍缺公开 longitudinal 数据(可跟进 IMC/SIGCOMM 网络测量 track)。
- Server Push 去留:RFC 9114 保留 PUSH 帧,但 Chrome 已默认禁用 HTTP/2 Push;HTTP/3 Push 是否会在 CDN 缓存预填充之外找到产品化场景,社区倾向 pessimistic。
动手试一下
理论讲完了,来实际体验一下 HTTP/3。最简单的方式是用 curl:
# 安装支持 HTTP/3 的 curl(Ubuntu)
sudo apt install curl # 7.88+ 内置 HTTP/3 支持
# 测试 HTTP/3 连接
curl --http3-only -I https://cloudflare-quic.com/
curl --http3 -v https://www.google.com/ 2>&1 | grep "using HTTP/3"
# 用 tc netem 模拟丢包测试
sudo tc qdisc add dev eth0 root netem loss 2% delay 50ms
# 对比 HTTP/2 vs HTTP/3
curl -w "time_total: %{time_total}s\n" --http2 https://example.com/
curl -w "time_total: %{time_total}s\n" --http3 https://example.com/
sudo tc qdisc del dev eth0 root总结
HTTP/3 不是 HTTP/2 的简单升级,它是为 QUIC 量身定制的应用层协议。把 HTTP/2 里那些为了弥补 TCP 缺陷而设计的机制全部拿掉,让传输层的归传输层、应用层的归应用层:
- 流多路复用:不再由 HTTP 层模拟,直接用 QUIC stream
- 流控:删掉,QUIC 自己做
- 头部压缩:从 HPACK 换成 QPACK,兼容无序传输
- 帧格式:简化,去掉冗余字段
现实中,HTTP/3 在高丢包、连接迁移频繁的场景更有机制优势;低丢包、长连接 bulk 传输下差距可能不明显。部署障碍主要是 UDP 可达性与发现链路——需在本用户群上度量降级率,而非假设「大厂已推即全网畅通」。
同主题延伸:QUIC 上 · QUIC 下 · TLS 1.3 工程实践 · TLS 1.3 最小实现
参考资料
规范
- RFC 9114 — HTTP/3
- RFC 9204 — QPACK
- RFC 9218 — Extensible Priorities
- RFC 7838 — Alt-Svc
- RFC 9460 — HTTPS RR
- RFC 9000 / RFC 9001 / RFC 9002 — QUIC 传输、TLS 映射、CC
- RFC 8446 — TLS 1.3
核心论文 / 部署报告
- Langley et al., The QUIC Transport Protocol: Design and Internet-Scale Deployment, SIGCOMM 2017.
- Kakhki et al., Taking a Long Look at QUIC, IMC 2017.
- Cardwell et al., BBR: Congestion-Based Congestion Control, ACM Queue 2016.
源码 / 工具
实验 / 自测
- 本文
curl --http3/tshark/tc netem命令需在目标环境自行执行;第五节不包含本站 benchmark 表。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【QUIC 协议拆解】QUIC 协议拆解(上):为什么 TCP 改不动了
打开一个网页要握手几次?TCP 三次 + TLS 一次 = 至少 2 RTT。QUIC 说:我一次搞定,重连甚至 0 次。不是 TCP 不够好,是它的基因决定了它改不动。
网络工程索引
汇总本站网络工程系列文章,覆盖分层模型、以太网、IP、TCP、DNS、TLS、HTTP/2/3、CDN、BGP 与故障诊断。
【网络工程】QUIC 生态与工程部署:从实验到生产
QUIC 已经不是实验性协议——HTTP/3 标准化后,CDN、浏览器和主流服务端框架都在推进 QUIC 支持。本文从工程视角对比主流 QUIC 库的成熟度和性能特征,讲解 CDN/负载均衡器的 QUIC 适配方案、从 TCP 迁移到 QUIC 的渐进路径、QUIC 调试工具链,以及生产环境的部署陷阱和性能调优实践。
【网络工程】可靠 UDP 框架:KCP、ENet 与 QUIC 的设计对比
TCP 的可靠传输是一种固定策略——全量有序、丢包即重传、拥塞窗口统一管理。但很多场景需要'可定制的可靠性':游戏要低延迟重传、视频要部分可靠、RPC 要多路复用无队头阻塞。本文深入对比 KCP、ENet、QUIC 三个在 UDP 上构建可靠传输的框架,剖析它们的 ARQ 策略、流控设计和工程取舍。