HTTP/3

本页目录

HTTP/3 将 HTTP 语义映射到 QUIC,使用 QUIC 提供的可靠流、流控、拥塞控制和 TLS 1.3 握手。它减少一个流的传输缺口对其他流有序交付的牵连,但不会消除所有跨请求依赖。

方法、状态码、缓存等继承的是 HTTP 通用语义;HTTP/2 的9 B帧头、流窗口更新、优先级树等并未原样搬过来。排查时应区分 HTTP 层、QUIC 流、QUIC 包与 UDP 数据报。

问题关键边界
UDP 丢包是否丢失 HTTP 内容QUIC 负责可靠流数据恢复
流彼此是否完全独立有序交付独立,但拥塞、额度和上层依赖仍共享
QPACK 表更新没到怎么办引用未到表项的字段块可能阻塞
HTTP/3 帧中 Stream ID 在哪里所属 QUIC 流已提供关联,不在 HTTP 帧头重复编码
第一次访问必须先走 HTTP/2 吗还可以通过 DNS HTTPS 等发现端点
切换网络或 0-RTT 是否保证更快取决于路径、状态、策略与实际请求

流映射:请求、控制、压缩指令分工

HTTP/3 把不同职责放到不同 QUIC 流 请求流:客户端双向流 0、4、8… 每流一次请求与响应;HEADERS / DATA 控制流:每端一条单向流 流类型 0x00;先 SETTINGS,再其他适用控制帧 QPACK 流:编码器、解码器分别单向 流类型 0x02 / 0x03;表更新与确认指令 QUIC Stream ID 与 HTTP/3 单向流类型是不同字段;类型 0x00 不意味着 QUIC 流号为 0。

请求使用客户端发起的双向 QUIC 流,ID 为0、4、8等;请求与响应分别占该流的两个方向。与 HTTP/2 不同,QUIC 流0可以承载业务请求,它不是连接控制帧专用流。

单向流开头先写 stream type,再承载该类型规定的内容。每端的控制流以 SETTINGS 为第一帧,同一控制流不能再次发送 SETTINGS;不存在 HTTP/2 那样的 SETTINGS ACK。QPACK 编码器和解码器流承载各自的指令格式,不是包在 HTTP HEADERS 或 DATA 帧里的表更新。

双方需允许对端建立至少三条单向流,为控制和 QPACK 留出空间及流控预算。已建立的关键流若意外关闭,会导致连接错误,不能把它当作完成一条普通请求后自然关闭。RFC 9114:流映射

帧头是变长整数,不再固定 9 B

HTTP/3 帧包含 Type、Length 两个 QUIC 变长整数以及 payload。长度只描述 payload,流ID由外层 QUIC 流提供。一般帧整数可以采用协议允许的编码长度,本例选最短编码。

取值范围编码字节最高两位
0 到 63100
64 到 16,383201
16,384 到 1,073,741,823410
再到 2^62−1811

5 B DATA 的 HTTP/3 帧为 Type 1 B + Length 1 B +内容5 B,共7 B。1000 B DATA 则是1+2+1000=1003 B;这些数字不包含 QUIC STREAM 帧、包保护、UDP/IP 头,不能直接拿来声称端到端节省了固定比例。

def varint(value):
    if not 0 <= value < 2**62:
        raise ValueError('out of range')
    for size, prefix in [(1, 0), (2, 1), (4, 2), (8, 3)]:
        if value < 2**(8 * size - 2):
            return (value | (prefix << (8 * size - 2))).to_bytes(size, 'big')

def decode(data):
    size = 1 << (data[0] >> 6)
    if len(data) < size:
        raise ValueError('truncated')
    return int.from_bytes(data[:size], 'big') & ((1 << (8 * size - 2)) - 1), size

for n in [0, 63, 64, 16383, 16384, 2**30 - 1, 2**30, 2**62 - 1]:
    value, used = decode(varint(n))
    assert value == n and used == len(varint(n))
assert varint(64).hex() == '4040'
small = varint(0) + varint(5) + b'hello'
large = varint(0) + varint(1000) + b'x' * 1000
assert small.hex() == '000568656c6c6f' and len(small) == 7
assert len(large) == 1003
try:
    decode(bytes([0x40]))
except ValueError:
    pass
else:
    raise AssertionError('accepted truncated integer')
print('HTTP/3 DATA 完整帧字节:', len(small), len(large))

请求或响应使用 HEADERS、可选 DATA 和 trailer HEADERS,结束方向由 QUIC 流结束表达,不存在 HTTP/2 的 END_STREAM 标志。HEADERS 帧可跨多个 QUIC STREAM 帧或包,不需要 CONTINUATION;包边界不是字段块边界。

少了 TCP 队头阻塞,仍需面对共享资源

假设流0缺少一段内容,而流4完整数据已到,QUIC 可以继续交付流4;它不需要等待流0的缺口。但同一个丢失包可能装有多个流的数据,且丢包引起的拥塞窗口调整可能影响整个连接发送。

连接级 MAX_DATA、单流 MAX_STREAM_DATA、可开流数量、CPU和服务端排队也可能限制进度。HTTP/3 将相应流控交给 QUIC,不再发送 HTTP/2 WINDOW_UPDATE;与 HTTP/2 只对 DATA payload 计费不同,QUIC 流额度覆盖该流的字节,包括 HTTP 帧头和 HEADERS 数据。

因此“某一请求不因另一流字节缺口而被迫等待”是具体保证;“请求间完全独立”则过度扩大了它。QUIC 流与流控

QPACK:在压缩收益与阻塞风险之间选择

QPACK 允许选择依赖,也允许依赖导致有限阻塞 编码器流:插入第 6、7 项 途中延迟;接收端暂时只插入 5 项 请求 A:Required Insert Count=7 HEADERS 已到,但暂时不能解码 请求 B:无动态表引用 Required Insert Count=0 表更新到达后 累计插入数达到 7,A 可继续 B 可独立解码不代表完全没有共享影响:连接拥塞、流控、CPU 与应用资源仍共享。

HPACK 依赖有序连接中的压缩状态演进;HTTP/3 流可独立交付,所以 QPACK 将表更新和确认分离到专用流。字段块仍可能引用尚未收到的动态项,并通过 Required Insert Count 表达解码依赖。

图中解码器累计只插入5项,请求A要求7项,就需等待更新;请求B只用静态项或字面量,Required Insert Count=0,可以解码。这里是已经还原的插入计数,线上字段还涉及规定的计数编码和 Base/相对索引,不能把简单整数比较当完整解码器。

QPACK 静态表有99项,索引和 HPACK 不通用。动态表仍按连接的编码方向分别维护;解码器返回 Section Acknowledgment、Stream Cancellation、Insert Count Increment 等信息,帮助编码器跟踪依赖和可释放状态。这些指令的单位和释放条件不同,不能统称为一次“表同步”。

SETTINGS_QPACK_MAX_TABLE_CAPACITY 与 SETTINGS_QPACK_BLOCKED_STREAMS 默认都为0。允许的阻塞流数不是“所有正在请求的流数”;编码器可选已知安全的引用或字面量,以更低压缩率避免增加潜在阻塞。Never Indexed 也不等于加密字段。RFC 9204:QPACK

流字节已齐,为什么字段仍不能解码?

先递送A的全部HEADERS,它仍等待第6、7次插入;递送B却立即可解码。补到6时A仍不满足依赖,补到7才解锁。这里的等待发生在QPACK依赖上,不能归因于A的流字节缺口。

正在呈现知识画面
流字节已齐,为什么字段仍不能解码?

可靠流交付完成不等于字段解码完成。只有引用动态项的块等待相应插入,静态/字面量块可以先完成。

发现端点:Alt-Svc 与 DNS HTTPS

Alt-Svc: h3=":443"; ma=86400 表示原服务提供一个同主机443端口的 HTTP/3 替代服务,max-age描述替代服务信息的缓存时间。它不是 HTTP 内容缓存时长,也不要求客户端立即把已有连接迁过去。

替代服务不改变原始 origin。即使连接到其他主机名或端口,认证仍需证明有权代表原服务;不能把一次位置提示当作自动信任新身份的授权。RFC 7838:替代服务

DNS HTTPS/SVCB 记录也可提供 alpn、port 等服务参数,帮助客户端在访问前发现 h3,不必每次先通过 h1/h2 收到 Alt-Svc。记录只是发现材料,客户端能力、证书验证及连接结果仍要满足要求。RFC 9460

UDP 被阻断、版本不兼容或路径不可达时,客户端可能竞速或回退到 TCP 上的 HTTP。具体等待时长由实现决定,不能承诺“尝试 QUIC 失败完全没有延迟”。指标应拆开首次发现、建连、回退和业务处理时间。

0-RTT 与迁移:依赖先前状态与路径验证

0-RTT 需要可用的恢复状态,并可能被服务器拒绝。发送早期请求时还要使用适当的已记忆 HTTP/3 设置和传输参数;拒绝后何时重发、是否可以重发都要遵守协议及业务约定。

早期数据具有重放风险。GET 名义上安全,不证明实际应用绝无副作用;扣款、提交订单等操作不能仅因延迟更低就放进early data。HTTP 的 Early-Data 标记和425 Too Early 可帮助中间件与源站协调,425后的重试不应再次放到early data中。RFC 8470

连接迁移可以在地址变化后保持连接身份,但要求新路径可用、验证及连接状态仍在,并受端点策略限制。Wi-Fi切换到蜂窝时可能经历探测、拥塞状态调整或失败,不能保证每次切换都无中断。服务端负载均衡也需把适当CID路由到持有连接状态的实例。QUIC 迁移

取消与 GOAWAY:边界和 HTTP/2 不同

QUIC 的 RESET_STREAM、STOP_SENDING 等参与请求终止,但“取消流”不等于业务事务回滚。没有响应时,需要判断请求是否可能已经被处理,再决定业务重试。

服务端 HTTP/3 GOAWAY 中的请求流ID是“不再接受”的边界:大于或等于它的请求不会被处理,小于它的请求可能已经处理。若边界为8,流8、12可在其他连接重试,流0、4则不能仅据此认定未执行。HTTP/2 的 Last-Stream-ID 是可能已处理的包含式上界,切勿复制成同一种比较运算。

多次GOAWAY的边界不能增加。客户端发出的GOAWAY使用推送ID,不能把它直接解释成同样的客户端请求流ID。RFC 9114:退出

推送、DATAGRAM 与观测

HTTP/3 基础规范仍定义推送和相应许可控制,浏览器或服务器产品是否支持另行核实;不能直接写成协议已废弃全部推送。普通 HTTP DATA 仍走可靠流,QUIC支持DATAGRAM不意味着所有HTTP/3响应自动变成不可靠传输。

抓包首先确认UDP上的QUIC、版本与连接建立,再结合可用的解密材料或实现事件追踪查看HTTP帧、QPACK阻塞和流控;仅凭UDP端口443不能判断请求成功。应用状态码与QUIC传输关闭错误也应分开记录,才能区分业务拒绝与连接故障。

继续查阅

  • HTTP/2:帧、多路复用、HPACK与退出边界对照。
  • UDP 与 QUIC:包恢复、CID与流控。
  • TLS 1.3:握手与early data安全属性。