HTTP/2 与 HTTP/3:复用、队头阻塞与 QUIC 的真实边界
HTTP/2 和 HTTP/3 沿用方法、状态码、资源与缓存语义,改变的是消息编码和传输组织。一条连接中可以同时存在多个请求流,连接数因此不再直接反映正在处理的请求数。
HTTP/1.1:文本消息 → TCP
HTTP/2 :HTTP 帧与多个流 → TLS(常见部署)→ TCP
HTTP/3 :HTTP 帧与请求流 → QUIC(集成 TLS 1.3)→ UDPQUIC 提供可靠、有序的单流交付、流量控制与拥塞控制。HTTP/3 使用 UDP 承载 QUIC 数据报,并不把 HTTP 请求简单改成不可靠 UDP 消息。
HTTP 消息如何分配到连接与流
从 HTTP/1.1 到 HTTP/2
HTTP/1.1 持久连接可以连续承载多次请求。流水线允许先发送多个请求,但响应顺序受到约束,前一个慢响应会挡住后续响应;浏览器历史上常通过多个连接增加并行度。
HTTP/2 把消息拆成带流标识的帧。不同请求的 HEADERS、DATA 可以交错发送,接收端按 stream ID 归入各自消息,不必按请求发出顺序完成所有响应。
同一 HTTP/2 连接中,客户端发送请求的一侧
├─ stream 1:HEADERS → DATA(END_STREAM=1)
├─ stream 3:HEADERS → DATA → DATA(END_STREAM=1)
└─ stream 5:HEADERS(END_STREAM=1,无请求正文)
连接上实际交错:1-H,3-H,1-D,5-H,3-D……END_STREAM 是 HEADERS 或 DATA 帧上的标志位,不是独立帧。发送方用它结束当前流的发送方向;例如请求发送完毕后,客户端仍可在同一流接收响应。双方的发送方向都结束,流才正常关闭。
请求使用 :method、:scheme、:authority、:path 等伪头字段,响应使用 :status。它们有顺序和使用规则,不是可以任意放到 HTTP/1.1 Header 中的字段;普通字段名在 HTTP/2 中使用小写。HTTP/2 规范
SETTINGS 交换对端参数;WINDOW_UPDATE 提供流量控制额度;RST_STREAM 终止一个流;GOAWAY 表示连接进入不再接受某些新请求的阶段。流结束只结束该流,连接可以继续服务其他请求。
压缩字段与限制资源
HPACK 使用静态表、动态表和编码规则压缩 HTTP/2 字段。它压缩 Header,不压缩 JSON 或文件内容;内容压缩仍由 Content-Encoding 等机制决定。HPACK
动态表具有连接级状态。实现需限制表容量、解码后字段大小、字段个数和连续帧处理成本,不能仅限制线上压缩字节数。一个很短的压缩块可能展开成更大的字段集合。
HTTP/2 的 DATA 受到连接级与流级窗口限制。增加最大并发流数也会增加请求对象、缓冲和业务并发,不能把服务端数据库连接池仍只有几十个连接的事实忽略掉。窗口、并发流和业务队列应按各自消耗的资源分别配置。
TCP 缺口为什么影响多个 HTTP/2 流
TCP 向应用交付连续字节。缺失一段 TCP 数据时,即使后面的段携带另一个 HTTP/2 流的内容,也可能需要等缺口恢复才能交付 HTTP/2 解析器。
TCP 字节顺序: [流1数据] [缺失片段] [流3数据] [流5数据]
应用可读位置: ----------↑
后续字节等待连续序列恢复影响范围取决于缺口位置、哪些数据已经交付、窗口和应用状态,不能用“丢一个包固定阻塞三个流”的整数公式描述真实行为。HTTP/2 消除了消息响应顺序造成的一类等待,底层共享 TCP 顺序仍然存在。
QUIC 与 HTTP/3 怎样组织传输
包号、流偏移与独立交付
QUIC 区分数据包编号和流内字节偏移。丢失数据的恢复可以放进新的数据包发送,不复用旧包号;接收方按各流的偏移重组字节。一个流存在缺口时,其他已具备连续数据的流可独立交付。QUIC 传输
QUIC 连接
├─ 请求流 A:offset 0… → 缺口 → 后续数据等待
├─ 请求流 B:offset 0… → 连续数据可交付
├─ HTTP 控制流
└─ QPACK 编码器 / 解码器流连接仍共享网络带宽、拥塞控制和部分资源限额。同一个 UDP 数据报也可能承载多个流的数据,丢包可以同时影响它们。发生拥塞后整体发送速度可能下降,因此“流独立交付”不意味着其他请求完全不受影响。QUIC 丢包检测与拥塞控制
HTTP/3 将请求映射到客户端发起的双向 QUIC 流,控制信息使用专门的单向流。其帧格式与 HTTP/2 不同,流控制由 QUIC 承担,不能把 HTTP/2 的 WINDOW_UPDATE 原样搬过来。HTTP/3 规范
QPACK 保留压缩,也管理解码依赖
QPACK 调整了 HPACK 对整体有序传输的依赖。编码器可以通过专门流更新动态表,请求字段块引用相应条目;若解码端尚未收到所需条目,该字段块仍可能等待。
实现通过动态表容量、允许阻塞的流数及编码选择,权衡压缩率与等待。静态表或字面值能减少这类动态依赖,但可能增加字节量。QPACK
因此 HTTP/3 的等待可能发生在不同位置:某请求流丢包、QPACK 条目未到、连接额度不足、业务线程排队或慢下游。排障应找到实际停止推进的位置,不用“已经上 HTTP/3”替代定位。
建连、迁移与网络设备
QUIC 使用 TLS 1.3 建立密钥并认证服务,允许在受控条件下进行会话恢复。0-RTT 仍有重放风险,副作用请求的处理要求见 DNS、TLS 与 HTTPS。
连接 ID 帮助 QUIC 在地址变化时识别连接,路径验证用于确认新路径。Wi-Fi 切换到蜂窝网络是否保持连接,还取决于客户端、服务端策略和网络可达性,不是所有部署都自动支持任意迁移。
生产入口通常需要同时开放 TCP 443 与 UDP 443。负载均衡器、防火墙、NAT 超时和 UDP 缓冲都会影响 QUIC;只配置 TCP 转发时,HTTP/2 可以正常工作而 HTTP/3 无法连接。支持旧协议的回退路径仍有实际价值。
运行真实 HTTP/1.1、HTTP/2 与 HTTP/3
镜像、证书和客户端能力
下载协议实验包,解压进入 http2-http3。Linux Bash、Docker Engine、可操作 Docker 的普通账号即可运行;另需按 TCP 工具环境构建 local/network-web-tools:1。
实验服务器使用 Caddy 2.11.4,curl 使用工具镜像中的发行版构建,HTTP/3 客户端使用 aioquic 1.2.0。工具镜像构建时系统包可能更新,先检查实际能力:
docker run --rm local/network-web-tools:1 curl --version
docker run --rm local/network-web-tools:1 python3 -c \
'import aioquic; print(aioquic.__version__)'本次 curl 8.18.0 的 Features 包含 HTTP2,不含 HTTP3;另检查的官方 curl 8.21.0 镜像也未包含 HTTP3。版本较新不保证编译进了全部传输后端。HTTP/3 使用专门客户端完成真实握手,不以一个不支持的 curl 参数冒充成功。curl HTTP/3 文档
实验文件:
http2-http3/
├─ Dockerfile 基于官方 Caddy 的最小派生镜像
├─ Caddyfile 同时提供 h1、h2、h3
├─ Caddyfile-tcp-only 仅保留 h1、h2
├─ make-certs.sh 新建实验 CA 与服务器证书
├─ h3_client.py 验证 CA、名称和实际 h3 协议
├─ Fallback.java Java 客户端首选协议与实际协议对照
└─ run.sh 正常、关闭 h3、恢复 h3 的完整流程Caddy 官方镜像的二进制带有绑定低端口的文件 capability。实验改用 18443,并在派生镜像构建阶段移除该 capability,运行时采用宿主 UID/GID、只读根文件系统和 --cap-drop ALL。构建的 root 步骤与运行身份分别说明,避免因能力集合冲突而出现 exec operation not permitted。Caddy 官方镜像
首次成功与实际协议输出
以下命令在同一个 Linux Bash 会话中执行。变量分别保存源码位置、运行身份、临时证书目录和本轮 Docker 资源名称;不修改宿主信任库,也不发布宿主端口。任一步失败时先处理该步,不继续比较协议。
SRC=$(pwd -P)
RUN_AS="$(id -u):$(id -g)"
WORK=$(mktemp -d /tmp/h3-manual.XXXXXX)
NETWORK=h3-manual-${WORK##*.}
SERVER=$NETWORK-server
docker image inspect local/network-web-tools:1 >/dev/null
docker build -t local/network-h3-server:1 "$SRC"构建成功后,生成仅供本次实验使用的 CA 和服务器证书,再创建网络:
docker run --rm --user "$RUN_AS" \
-v "$WORK:/work" -v "$SRC:/src:ro" \
local/network-web-tools:1 bash /src/make-certs.sh /work/certs
docker network create "$NETWORK"证书命令应输出 /work/certs/server.pem: OK。make-certs.sh 使用 umask 077 限制密钥文件访问,服务器与生成证书的容器采用同一 UID/GID。镜像首次下载需要联网;内网可先导入经过核验的镜像归档。不要用关闭证书验证替代正确的证书与挂载权限。
启动提供三种协议的服务:
docker run -d --name "$SERVER" --network "$NETWORK" --network-alias h3-server \
--user "$RUN_AS" --read-only --cap-drop ALL --security-opt no-new-privileges \
--tmpfs /data:rw,mode=1777 --tmpfs /config:rw,mode=1777 \
-v "$WORK/certs:/certs:ro" -v "$SRC/Caddyfile:/etc/caddy/Caddyfile:ro" \
local/network-h3-server:1
docker logs "$SERVER"日志应显示服务器已经运行,协议包含 h1、h2、h3。若容器退出,先检查 docker inspect "$SERVER" 的 State 和日志,再发请求。
先强制 HTTP/1.1,再首选 HTTP/2;每次同时查看服务端回报、客户端实际协议及状态码:
docker run --rm --network "$NETWORK" --user "$RUN_AS" \
-v "$WORK/certs/ca.pem:/ca.pem:ro" local/network-web-tools:1 \
curl -q --noproxy '*' --silent --show-error --fail-with-body --max-time 5 \
--cacert /ca.pem --connect-to api.example.test:18443:h3-server:18443 \
--http1.1 -w '|%{http_version}|%{http_code}\n' https://api.example.test:18443/
docker run --rm --network "$NETWORK" --user "$RUN_AS" \
-v "$WORK/certs/ca.pem:/ca.pem:ro" local/network-web-tools:1 \
curl -q --noproxy '*' --silent --show-error --fail-with-body --max-time 5 \
--cacert /ca.pem --connect-to api.example.test:18443:h3-server:18443 \
--http2 -w '|%{http_version}|%{http_code}\n' https://api.example.test:18443/两条命令都应成功退出,分别输出 HTTP/1.1|1.1|200 与 HTTP/2.0|2|200。若第二条仍输出 1.1,说明发生了回退,不能视为 HTTP/2 成功。再发起 QUIC 请求:
docker run --rm --network "$NETWORK" --user "$RUN_AS" \
-v "$WORK/certs/ca.pem:/ca.pem:ro" -v "$SRC:/src:ro" \
local/network-web-tools:1 python3 /src/h3_client.py --ca /ca.pem应输出 alpn=h3 status=200 server=HTTP/3.0 并成功退出。这里同时检查握手 ALPN、状态码以及服务器对请求协议的回报。
Caddyfile 的完整配置如下:
{
admin off
auto_https off
servers {
protocols h1 h2 h3
}
}
https://api.example.test:18443 {
tls /certs/server.pem /certs/server.key
respond "{http.request.proto}" 200
}这里显式加载本地证书,不申请公网证书;关闭自动 HTTPS 管理没有关闭站点的 TLS。protocols 决定允许的协议,服务器模板返回本次实际请求版本。Caddy 协议选项
HTTP/2 客户端使用 --connect-to 将 api.example.test:18443 的连接目标指向实验网络中的 h3-server:18443。该选项只改变连接目标,证书校验仍使用 URL 的服务名;-q、--noproxy '*' 和 --cacert 控制配置、代理与信任来源。复验脚本还会断言响应分别为 HTTP/1.1|1.1|200 与 HTTP/2.0|2|200,发生降级时会失败。
HTTP/3 客户端具体检查什么
客户端使用标准 aioquic 配置开启证书验证:
configuration = QuicConfiguration(
is_client=True,
alpn_protocols=H3_ALPN,
server_name="api.example.test",
idle_timeout=2.0,
)
configuration.load_verify_locations(cafile=args.ca)它连接 Docker 服务名 h3-server,但将原始服务名 api.example.test 用于身份验证。HandshakeCompleted 给出协商的 ALPN,H3Connection 把 QUIC 事件转换成 HeadersReceived 和 DataReceived。程序收齐流末尾后检查 status=200、ALPN=h3 及 HTTP/3.0 正文;累计响应正文最多 4096 字节,该判断不包含响应头与 QUIC 协议开销,整个操作限制为 6 秒。aioquic asyncio API、HTTP/3 API
这是一个只用于 GET 的小客户端,没有实现浏览器缓存、代理发现、自动协议竞速和业务重试。Python 负责调用 QUIC 库,应用协议的检查方式与 Java 或其他语言使用成熟客户端时相同。
关闭 QUIC,再恢复
保留证书与网络,停止本轮服务器,再用 Caddyfile-tcp-only 启动同名实例。配置的唯一协议差异为 protocols h1 h2:
docker stop -t 5 "$SERVER"
docker rm "$SERVER"
docker run -d --name "$SERVER" --network "$NETWORK" --network-alias h3-server \
--user "$RUN_AS" --read-only --cap-drop ALL --security-opt no-new-privileges \
--tmpfs /data:rw,mode=1777 --tmpfs /config:rw,mode=1777 \
-v "$WORK/certs:/certs:ro" -v "$SRC/Caddyfile-tcp-only:/etc/caddy/Caddyfile:ro" \
local/network-h3-server:1
docker logs "$SERVER"日志显示服务运行后,重复上一节的 HTTP/2 命令,仍应得到 HTTP/2.0|2|200。然后检查 HTTP/3 的失败退出码;这里用条件分支捕获预期失败,即使当前 Bash 开启了 set -e 也可执行:
if docker run --rm --network "$NETWORK" --user "$RUN_AS" \
-v "$WORK/certs/ca.pem:/ca.pem:ro" -v "$SRC:/src:ro" \
local/network-web-tools:1 python3 /src/h3_client.py --ca /ca.pem; then
H3_EXIT=0
else
H3_EXIT=$?
fi
printf 'http3Exit=%s\n' "$H3_EXIT"
test "$H3_EXIT" -eq 2预期看到 http3Failed=TimeoutError 或 http3Failed=ConnectionError,最后的检查通过。退出码 125、126、127 等 Docker 启动错误不算协议负例成功。此时 HTTP/2 对照请求正常,而同一身份、证书和目标下的 HTTP/3 连接失败。
恢复三协议配置,再用同一个 HTTP/3 客户端请求:
docker stop -t 5 "$SERVER"
docker rm "$SERVER"
docker run -d --name "$SERVER" --network "$NETWORK" --network-alias h3-server \
--user "$RUN_AS" --read-only --cap-drop ALL --security-opt no-new-privileges \
--tmpfs /data:rw,mode=1777 --tmpfs /config:rw,mode=1777 \
-v "$WORK/certs:/certs:ro" -v "$SRC/Caddyfile:/etc/caddy/Caddyfile:ro" \
local/network-h3-server:1
docker logs "$SERVER"确认日志再次显示服务器运行且协议包含 h3 后,再发请求:
docker run --rm --network "$NETWORK" --user "$RUN_AS" \
-v "$WORK/certs/ca.pem:/ca.pem:ro" -v "$SRC:/src:ro" \
local/network-web-tools:1 python3 /src/h3_client.py --ca /ca.pem服务启动后应重新得到 alpn=h3 status=200 server=HTTP/3.0。若仍失败,按下表检查,不把重启命令成功当成协议已经恢复。
这个负例模拟服务端未提供 QUIC,不是注入丢包,也没有测量丢包下的延迟收益。它回答的是“TCP 路径正常时,QUIC 路径是否可能失败”。要测试 UDP 丢包、RTT 和吞吐,应在专门隔离网络中控制这些变量,重复采样,不能拿本机握手耗时宣称生产性能提升。
| 未出现预期结果 | 检查 | 下一步 |
|---|---|---|
| 工具镜像不存在 | docker image inspect 与构建日志 | 按工具源码先构建,或导入批准的归档 |
| 证书访问被拒绝 | 宿主 UID/GID 与临时目录权限 | 保持生成和运行身份一致,不把私钥改成公开可读 |
| HTTP/2 实际为 1.1 | curl Features、ALPN 与服务端 protocols | 补齐双方能力,再确认实际版本 |
| 正常 h3 仍失败 | Caddy 日志、UDP监听、客户端证书错误 | 分开处理网络与信任,禁止关闭验证求成功 |
| TCP-only 负例也成功 | 加载的 Caddyfile、旧实例、目标名称 | 确认实验只连接本轮服务,再重跑 |
| 恢复后仍失败 | 旧配置、证书挂载与容器状态 | 检查启动日志,并用同一客户端复测 |
完成手动实验后,回收本轮实例、网络和临时证书;保留下载的镜像供以后复用:
docker stop -t 5 "$SERVER"
docker rm "$SERVER"
docker network rm "$NETWORK"
case "$WORK" in
/tmp/h3-manual.*) rm -r -- "$WORK" ;;
*) printf '临时目录不符合预期,请先核对 WORK\n' >&2 ;;
esac以后需要重复检查“正常→关闭 h3→恢复”三条路径,可在解压目录运行 bash run.sh。它把上述请求与版本断言串联起来,使用自己的资源名,并在退出时清理本轮资源;HTTP/2 回退、HTTP/3 意外成功或恢复失败都会使复验失败。
Java 的首选版本不是协商结果
JDK 25 的 HttpClient.Version 提供 HTTP_1_1 与 HTTP_2。配置 HTTP_2 表达客户端首选,不保证每个服务都接受它;实际版本必须读取 HttpResponse.version()。Java HttpClient
Fallback.java 启动仅支持 HTTP/1.1 的 JDK HttpServer,再让首选 HTTP/2 的客户端请求它。在源码目录执行:
docker run --rm --user 10001:10001 --read-only --network none \
--tmpfs /tmp:rw,exec,size=128m -v "$PWD:/src:ro" \
eclipse-temurin:25.0.4_7-jdk bash -ec \
'javac --release 17 -Xlint:all -Werror -d /tmp /src/Fallback.java
java -cp /tmp Fallback'结果为 preferred=HTTP_2 actual=HTTP_1_1 status=204。该实验观察回退,不用它推断服务已经提供 HTTP/2。反向代理的下游和上游也是两条独立连接,浏览器显示 h3 时,上游应用仍可能收到 HTTP/1.1。
协商、回退与运行排查
客户端怎样发现 HTTP/3
HTTP/2 的 HTTPS 部署通常通过 ALPN 协商。HTTP/3 可以通过 Alt-Svc、支持的 HTTPS DNS 记录或预配置发现可用端点。Alt-Svc 表示替代服务位置与有效期,连接到替代端点后仍需验证原始服务的身份。替代服务规范
首次访问可能先走 HTTP/2,再从响应获知 HTTP/3;缓存了替代服务信息后,后续选择可能不同。客户端可能并行尝试或回退,浏览器设置、网络策略和历史失败也会影响结果。诊断时保留实际协议、目的地址、连接复用及 Alt-Svc 状态,不只记录“启用了 QUIC”。
支持 HTTP/3 的 curl 中,--http3 与 --http3-only 的回退行为不同。使用任何选项之前先检查 curl --version;希望验证某协议时要检查最终 http_version,不能仅凭退出码为零宣称命中。
流取消、GOAWAY 与重试
HTTP/2 的 RST_STREAM 或 QUIC 的流终止可以让其他请求继续存在;连接级错误则可能同时影响许多流。大量请求共享一条连接减少了握手开销,也扩大了单连接故障的影响范围。
GOAWAY 帮助服务器停止接受新请求并排空已接受的工作。客户端需要结合协议给出的流标识和已收到的结果决定哪些请求可重试。收到流重置、超时或连接关闭时,业务写入可能已经执行;重试仍须遵循 超时、重试与幂等的规则。
取消网络等待也不会自动撤销数据库事务。客户端取消、服务器停止读取和后台任务停止应分别确认;长时间运行的操作最好有任务身份与可查询状态。
指标和 F12 的使用
在 F12 Network 请求表中显示 Protocol 列,查看 h2、h3 或 http/1.1。检查 Remote Address 与 Timing,确认实际连接位置和是否复用;代理、Service Worker 或缓存可能使这一行并非新的远程握手。Chrome Network 参考
服务端至少区分连接数、在途流数、等待流额度的请求数、流重置与连接错误。对于 HTTP/3,还应观察握手失败、路径变化和 UDP 相关丢失/缓冲信息。TLS 加密使普通抓包无法直接读取全部 HTTP 字段,库支持的 qlog 或服务端协议日志可能更适合分析,但日志仍需脱敏。
| 问题 | 更有用的对照 |
|---|---|
| 连接少了但内存上涨 | 同时在途流数、每流缓存和解码字段大小 |
| 部分请求等待很久 | 流窗口、连接窗口、业务队列与下游耗时 |
| HTTP/3 比 HTTP/2 慢 | 相同网络、相同连接复用条件下的握手、丢包、CPU与尾延迟 |
| 只有某网络无法 h3 | UDP可达性、NAT/防火墙与回退时间 |
| 发布时大量失败 | GOAWAY/排空、旧连接年龄和请求可重放性 |
升级协议应先保证方法、缓存和错误语义保持一致,再比较资源与延迟。收益取决于负载和网络条件,协议版本本身不是性能成绩。
权威资料与规范地址
按协议、系统调用与 Java API 查阅完整定义。
