HTTP/3 把 HTTP 搬到 QUIC 上,QUIC 又建在 UDP 之上:合并握手、每个流独立丢包恢复、全程加密。这篇用 aioquic 对真实服务器发 HTTP/3 请求,抓包看 QUIC 的包头和 1200 字节填充,量握手省下的一个往返;在本机服务端上实测 0-RTT 再省一个往返;最后在丢包下和 curl + Caddy 的原生实现对照,看 aioquic 的 13 秒慢在哪里。
HTTP/3:ALPN h3,握手(到可发请求)逐次 [194, 167, 325, 169, 171] ms,中位 171;请求发出到首字节 中位 163 ms;总计 中位 348 ms;状态 b'200',577 字节 HTTP/2:TCP 握手 中位 160 ms + TLS 握手 中位 164 ms + 请求到首字节 中位 343 ms;总计 中位 680 ms
QUIC 把传输握手和 TLS 握手合并,1 个 RTT 即可发请求;TCP + TLS 1.3 要 2 个。整次取回首页 348 ms 对 680 ms。
整个请求在线上:21 个 UDP 数据报,9418 字节(UDP 载荷);客户端→服务器 10 个 / 2984 字节;服务器→客户端 11 个 / 6434 字节
+ 0.0 ms C→S 1200 字节 长包头 Initial 版本 0x00000001 DCID 8 字节
+ 156.2 ms S→C 1200 字节 长包头 Initial 版本 0x00000001 DCID 8 字节
+ 157.0 ms C→S 1200 字节 长包头 Initial 版本 0x00000001 DCID 20 字节
+ 158.6 ms S→C 1200 字节 长包头 Handshake 版本 0x00000001 DCID 8 字节
+ 158.6 ms S→C 1200 字节 长包头 Handshake 版本 0x00000001 DCID 8 字节
+ 158.6 ms S→C 1200 字节 长包头 Handshake 版本 0x00000001 DCID 8 字节
+ 158.6 ms S→C 552 字节 长包头 Handshake 版本 0x00000001 DCID 8 字节
+ 159.0 ms C→S 60 字节 长包头 Handshake 版本 0x00000001 DCID 20 字节
+ 159.1 ms C→S 60 字节 长包头 Handshake 版本 0x00000001 DCID 20 字节
+ 159.2 ms C→S 60 字节 长包头 Handshake 版本 0x00000001 DCID 20 字节
+ 164.9 ms C→S 213 字节 长包头 Handshake 版本 0x00000001 DCID 20 字节
+ 165.2 ms C→S 60 字节 短包头(1-RTT)
客户端第一个数据报 1200 字节(规范要求 Initial 所在的数据报至少 1200 字节,故意填充,防止 QUIC 服务器被拿来做流量放大);版本 0x00000001 = QUIC v1长包头(Initial、Handshake)带版本号和连接 ID,握手后用短包头;Initial 所在的数据报填充到 1200 字节。
第一次(完整握手):响应头 498 ms,状态 b'200';收到 2 个 session ticket;ticket 的 max_early_data_size = None
A 完整握手 响应头 逐次 [524, 524, 630, 480] 中位 524 ms;session_resumed = False,early_data_accepted = False,状态 [b'200']
B 带 ticket,等握手完成 响应头 逐次 [473, 562, 1352, 474] 中位 518 ms;session_resumed = True,early_data_accepted = False,状态 [b'200']
C 带 ticket,0-RTT 响应头 逐次 [610, 476, 615, 524] 中位 567 ms;session_resumed = True,early_data_accepted = False,状态 [b'200']example.com 会发 session ticket,带着它连能恢复会话,但 ticket 不允许 early data,所以没有 0-RTT。我最初没拿到 ticket 是自己代码的错误(回调要传给 connect())。
第一次(完整握手,同时保存 session ticket):响应头 218 ms,收完 525 ms;收到 1 个 ticket;ticket 里 max_early_data_size = 4294967295
A 完整握手 响应头 逐次 [206, 209, 218, 208, 210, 207] 中位 209 ms;收完 中位 519 ms;session_resumed = False,early_data_accepted = False
B 带 ticket,等握手完成 响应头 逐次 [215, 218, 204, 203, 209, 209] 中位 209 ms;收完 中位 514 ms;session_resumed = True,early_data_accepted = True
C 带 ticket,0-RTT 响应头 逐次 [105, 104, 106, 110, 107, 104] 中位 105 ms;收完 中位 461 ms;session_resumed = True,early_data_accepted = True
(往返 100 ms:A、B 应当约等于 2 个往返——1 个等握手,1 个等响应;C 约等于 1 个往返)自己的 aioquic 服务端开了 early data 后,0-RTT 请求在握手完成前就发出,响应头 105 ms 对 209 ms,约少一个往返(往返 100 ms)。
丢包率 协议 总耗时(ms)逐次 中位 第一个流完成 中位 最后一个流完成 中位
0% HTTP/2 [610, 611, 613, 708] 612 408 612
0% HTTP/3 [848, 842, 839, 838] 840 839 840
3% HTTP/2 [1498, 810, 915, 918] 917 462 917
3% HTTP/3 [10591, 15373, 15684, 13839] 14606 13948 14606最初的对比里 aioquic 对 aioquic 在 3% 丢包下要 14606 ms,比 HTTP/2 的 917 ms 慢十几倍,但这个数字主要来自 aioquic 这个纯 Python 实现。
自检:curl --http3-only 取 Caddy 的 100 KB 文件 → 200 3 100000 (状态码 / HTTP 版本 / 字节数)
丢包率 协议 客户端 → 服务端 总耗时(ms)逐次 中位
0% HTTP/3 aioquic 客户端 → aioquic 服务端 [842, 845, 846, 844] 844
0% HTTP/3 aioquic 客户端 → Caddy 服务端 [708, 695, 693, 696] 695
0% HTTP/3 curl 客户端 → aioquic 服务端 [579, 550, 540, 546] 548
0% HTTP/3 curl 客户端 → Caddy 服务端 [388, 392, 391, 396] 392
0% HTTP/2 python h2 客户端 → python h2 服务端 [710, 608, 708, 611] 659
0% HTTP/2 python h2 客户端 → Caddy 服务端 [611, 608, 608, 610] 609
0% HTTP/2 curl 客户端 → python h2 服务端 [449, 440, 444, 428] 442
0% HTTP/2 curl 客户端 → Caddy 服务端 [341, 337, 336, 343] 339
3% HTTP/3 aioquic 客户端 → aioquic 服务端 [11051, 13927, 13359, 13020] 13189
3% HTTP/3 aioquic 客户端 → Caddy 服务端 [7229, 7051, 6064, 8456] 7140
3% HTTP/3 curl 客户端 → aioquic 服务端 [1983, 1899, 1724, 2184] 1941
3% HTTP/3 curl 客户端 → Caddy 服务端 [913, 797, 1064, 1014] 964
3% HTTP/2 python h2 客户端 → python h2 服务端 [709, 908, 711, 716] 714
3% HTTP/2 python h2 客户端 → Caddy 服务端 [917, 1116, 1612, 1218] 1167
3% HTTP/2 curl 客户端 → python h2 服务端 [842, 562, 731, 731] 731
3% HTTP/2 curl 客户端 → Caddy 服务端 [926, 769, 547, 636] 702aioquic 对 aioquic 在 3% 丢包下 13189 ms;换成 curl 对 Caddy 后降到 964 ms,但仍比同样原生的 HTTP/2(702 ms)慢——在这个很小的测试里没有显示出 HTTP/3 在丢包下更好。