Theory · Network · HTTP × Protobuf
HTTP 与 Protobuf:底层原理与对比
一个管「怎么传」,一个管「怎么编码」—— HTTP 版本演进(1.1 / 2 / 3)× Protobuf wire format(TLV / varint / zigzag)× JSON 与 gRPC 的选型对比
HTTP 传输协议
文本报文 → 二进制分帧 → QUIC;队头阻塞的三层来源
Protobuf 序列化
schema 驱动 · tag=(编号<<3)|wire_type · varint/zigzag · 默认值省略
对比与选型
先对齐层次再比较:JSON↔Protobuf,HTTP/1.1↔H2↔H3;gRPC 全链路
这份 deck 解决一个高频混淆点:面试里说「HTTP 和 Protobuf 谁快」其实是跨层比较。HTTP 是应用层传输协议,Protobuf 是序列化格式,gRPC 只是「HTTP/2 + Protobuf」的组合包装。按「HTTP 原理 → Protobuf 编码 → 分层对比 → 选型 → QA」的顺序展开,所有规范细节都对照 RFC 原文和 protobuf.dev 核对过。
Big Picture
先把问题摆正:两者根本不在同一层
HTTP:应用层传输协议——定义请求/响应语义(方法、状态码、头部)、连接管理、缓存协商;不关心 body 里是什么字节。
Protobuf:序列化格式——定义「内存对象 ↔ 字节流」的双向映射;不关心字节跑在 HTTP、Kafka 还是落盘文件里。
这页是全 deck 的锚点。考官问「为什么不用 HTTP 而用 Protobuf」时,先指出这是层次错位:和 Protobuf 同层对比的是 JSON/XML;和 HTTP/1.1 同层对比的是 HTTP/2、HTTP/3 或 Thrift 等私有 RPC 协议。gRPC = HTTP/2 + Protobuf + LPM 帧,两者是叠加不是二选一。
HTTP · Fundamentals
HTTP 本质:请求-响应模型的文本协议,30 年演进一条线
- 报文模型不变:请求 =
method path version + 头部 + 空行 + body;1.1 是 ASCII 文本(协议本身可读),2/3 把「怎么传」二进制化,但方法、状态码、头部这些语义从未变。
- 无状态:协议层不记得上一个请求,会话状态靠 Cookie/Token 上移到应用层——这是 HTTP 能水平扩容的前提。
演进主线记三个转折:1.1 解决「每请求一条连接」的浪费(持久连接),2 解决「一条连接上请求排队」(多路复用),3 解决「TCP 本身拖后腿」(QUIC 换掉 TCP)。语义层(RFC 9110)一直稳定,变的只是传输和编码方式——这是 HTTP 生态三十年不倒的原因。
HTTP/1.1 · RFC 9112
HTTP/1.1 底层:持久连接救了浪费,救不了队头阻塞
做对了什么
- 持久连接成默认:RFC 9112 §9.3 原文「HTTP/1.1 defaults to the use of persistent connections」——一条 TCP 连接串行发多个请求,
Connection: close 才主动关。
- Host 头:一台服务器(一个 IP)托管多个域名 → 虚拟主机与 CDN 的基础。
- chunked 传输:body 不必先知道 Content-Length,边生成边发;服务器用它定界响应。
为什么快不起来
- 应用层队头阻塞:一条连接同一时刻只有一个请求在等响应;后发的请求必须排队。
- pipeline 名存实亡:规范允许(MAY),但要求响应按序返回、且 UA SHOULD NOT 在非幂等方法后 pipeline(§9.3.2)→ 浏览器从未默认启用。
- 实际解法是并行多开连接:浏览器对同一来源并行约 6 条连接(Chrome/Firefox 均 6)→ 催生域名分片、雪碧图、内联资源这些「1.1 时代的补丁」。
面试要点:说「HTTP/1.1 支持 pipeline」对,但要说全——它要求响应严格按请求序返回,队头请求慢,后面全部干等;非幂等方法(POST)后面 pipeline 还有重试歧义。所以行业用「多开 TCP 连接」绕过去,代价是握手、拥塞窗口、服务端内存全部 ×6。这个绕法正是 HTTP/2 多路复用的动机。
两个易错点:一是「1.1 默认长连接」——对的,keep-alive 不再需要显式声明;二是 pipeline 为何失败——响应必须按序返回,队头没解决反而引入重试安全问题,浏览器干脆不支持。把 6 连接限制和域名分片讲出来,能自然过渡到 HTTP/2 的设计动机。
HTTP/2 · RFC 9113 / RFC 7541
HTTP/2:二进制分帧 + 多路复用,队头阻塞只解决了一半
核心逻辑链:1.1 的痛点是排队 → H2 把请求响应切成帧、给每路请求编号(Stream ID),帧可交错发送、两端按编号重组,应用层队头阻塞消失。但要主动说出反面:TCP 只认字节流不认流编号,丢一个报文段,内核必须等重传才能交付后面的数据,所有流一起卡——这就是 H2 并没有「彻底解决」队头阻塞,为下一页 HTTP/3 埋下伏笔。HPACK 记三个部件:静态表 61、动态表、Huffman。
HTTP/3 · RFC 9114 / RFC 9000
HTTP/3:QUIC 重造传输层,把「流」从内核拿回用户态
QUIC(RFC 9000)改了传输层什么
- UDP 之上用户态实现:绕开内核 TCP 升级周期,协议进化由应用更新驱动。
- TLS 1.3 内置进握手(RFC 9001):传输与加密一体协商,建连 1-RTT,会话复用可 0-RTT 早期数据(注意:0-RTT 有重放风险,只放幂等请求)。
- 流间真正独立:每个 Stream 有独立的包号/确认/流控,单流丢包不再阻塞其他流 —— 消灭 TCP 层队头阻塞。
- Connection ID:连接身份与「源IP+源端口」四元组解耦 → WiFi 切 5G 连接迁移不断线。
HTTP/3(RFC 9114)适配了什么
- 语义照搬 HTTP:方法、状态码、头部字段与 1.1/2 完全一致,变的只是承载方式。
- 帧重新设计:QUIC 已提供可靠有序流,HTTP/3 帧不再需要 H2 的流 ID/长度那套。
- QPACK(RFC 9204)替代 HPACK:H2 的 HPACK 依赖帧按序到达,QUIC 流间乱序会让动态表错位 → QPACK 把动态表更新改为可乱序解码的设计,代价是额外往返。
- 服务发现:首个连接走 H2/H1,经
Alt-Svc 头或 HTTPS DNS 记录学到 H3 端点。
面试要点:H3 的收益在高丢包/弱网场景最大(流独立 + 更快的握手),在低丢包内网里和 H2 差距很小。Go 生态标准库长期未内置 HTTP/3,实际用 quic-go(quic-go/quic-go)做服务端与客户端。
讲透一个对比就能拿下这页:HPACK 的动态表假设帧严格有序,QUIC 天生乱序送达,所以头压缩必须重做成 QPACK——这是「上层设计受底层特性制约」的绝佳案例。0-RTT 记住安全代价(重放攻击),连接迁移记住本质(Connection ID 把身份与四元组解耦)。
Protobuf · Serialization
Protobuf 本质:把「字段名 → 字段编号」从运行时移到编译期
- JSON 自描述 vs Proto 靠 schema:JSON 的字段名随每条报文传输,任何端拿到字节就能读懂;Protobuf 字节里只有编号,没有 schema 就是一堆乱码 —— 这既是体积优势,也是可读性劣势。
- Go 侧落地:
google.golang.org/protobuf(v2 runtime)+ protoc-gen-go;生成代码带快速路径(直接按编号写 buffer),慢路径走反射元数据。
用一句话立住本质:JSON 把元数据(字段名)放在运行时的报文里,Protobuf 把元数据放在编译期的 schema 里,所以线上每条消息只需付「1~2 字节编号」的成本。强调编号即身份:字段名改动不影响线上,编号改动等于换字段——为兼容性一页做铺垫。
Protobuf · Wire Format
Wire format 总纲:TLV 结构,tag = (编号 << 3) | wire_type
150→08 96 01 是官方文档的例子,面试可直接手推:150 二进制 10010110 拆成两个 7 位组(低组 0010110、高组 0000001),低组加继续位得 0x96,高组补齐得 0x01;tag 字节 = 1<<3 | 0 = 0x08。tag 的低 3 位是 wire type,六种类型见下一页的表。
Protobuf · Wire Types
六种 wire type:解析端靠它跳过不认识的字段
tag 的低 3 位是 wire type,告诉解析端「这个字段的值怎么读、有多长」。遇到未知字段号时,解析端只需按 wire type 给出的长度跳过整个值——这就是 Protobuf 前向兼容的机制基础。
| wire type | 名称 | 含义 | 用于 |
| 0 | VARINT | 变长整数 | int32/64、uint32/64、sint32/64、bool、enum |
| 1 | I64 | 定长 8 字节 | fixed64、sfixed64、double |
| 2 | LEN | 长度前缀 + 内容 | string、bytes、嵌套 message、packed repeated |
| 3 / 4 | SGROUP / EGROUP | 组开始 / 结束 | group —— 已废弃(仅 proto2),解析端仍需容忍 |
| 5 | I32 | 定长 4 字节 | fixed32、sfixed32、float |
六种 wire type 里 3/4(SGROUP/EGROUP)已废弃但要知道——解析器靠 wire type 就能跳过不认识的字段,这是前向兼容的机制基础。被跳过的 unknown fields 旧版会丢弃、proto3 起默认保留在未知字段集里,能带出这一点是加分项。
Protobuf · Numbers
数值编码细节:varint 便宜,负数昂贵,zigzag 来救
varint 与它的坑
- Base 128 变长:小数值极省(0~127 只要 1 字节);业务里大多数 id/计数是小数,平均成本远低于定长。
- 负数是坑:int32/int64 的负数按 64 位符号扩展编码 → 恒占 10 字节(-1 也是 10 字节)。
- 解法 sint32/sint64:先做 ZigZag 变换再 varint。
- 定长类型:fixed64/double 用 I64(8 字节)、fixed32/float 用 I32(4 字节)——数值普遍很大(如时间戳毫秒、金额分)时比 varint 划算,且免变长解码。
ZigZag:把符号位「折」进最低位
- 公式:正数 p → 2p(偶数),负数 n → 2|n|−1(奇数);解码 (n >> 1) ^ −(n & 1)。
- 效果:−1 → 1(1 字节),0 → 0,1 → 2,−2 → 3,2 → 4 —— 绝对值小的有符号数都变短。
- 选型口诀:可能为负的整数用
sint32/sint64;确定非负用 int32/int64 或 uint32/64;大数值用 fixed。
- LEN 类型:string/bytes/嵌套 message = tag + varint(长度) + 原始字节;嵌套消息就是递归的 TLV,长度字段让解析器可以整体跳过。
// -1 在两种类型下的线上体积对比(手推必考)
int32 x = -1; // varint:FF FF FF FF FF FF FF FF FF 01 → 10 字节
sint32 x = -1; // ZigZag(-1)=1 → varint:01 → 1 字节(10 倍差距)
bool/enum // 同 VARINT;enum 的 0 值必须定义为「未知/缺省」语义
最常考的推演题就是「为什么负数 int32 占 10 字节」——因为 varint 编码的是补码按 64 位符号扩展后的无符号数,最高位全是 1。答出 sint 的 ZigZag 公式(p→2p,n→2|n|−1)并给出 −1→1 字节的对比,基本就是满分。fixed vs varint 的选择标准:数值分布普遍大就用定长。
Protobuf · Rules
组合类型与序列化规则:这些「默认行为」都是考点
repeated / map / 字段顺序
- repeated:同一字段重复出现 tag+payload;数值型在 proto3 默认 packed —— 整段元素装进一个 LEN 载荷,tag 摊薄成一个(解析端必须同时兼容 packed 与非 packed 两种形态)。
- map<K,V>:线上一律编码为 repeated message,field 1 = key、field 2 = value,迭代顺序不保证;不要依赖 map 的顺序做业务。
- 字段顺序不保证:规范明确序列化输出顺序是实现细节,parser 必须容忍任意字段顺序 → 同一消息两次序列化字节可能不同,别拿序列化结果做哈希/等值比较。
presence 与未知字段
- proto3 隐式 presence:标量字段等于默认值(0 / "" / false)时整段不写上 wire —— 副作用:「没设置」和「设置成 0」在线上无法区分。
- 需要区分就加 optional(显式 presence,官方推荐 basic types 一律加);消息类型字段天生有 presence。
- 未知字段:解析时遇到没见过的字段号,按 wire type 跳过并保留(proto3 自 3.5 起)→ 新版本消息穿过旧服务再转发不丢数据,这是滚动升级的安全网。
- oneof:互斥字段共用 presence,线上就是普通 TLV,只是语义层保证只有一个被设置。
高频追问:proto3 为什么去掉 required?——required 让「加字段」变成高风险操作(旧数据缺字段直接解析失败),proto3 全面转向「缺省即默认值」,用 optional 补回显式 presence。这解释了 proto2 → proto3 的整个设计取向:牺牲一点严格性,换取平滑演进。
三个默认行为容易被面试官当陷阱题:packed repeated 的兼容双形态、map 无序、序列化字节不稳定(不能当指纹用)。presence 那组要能说出「0 值消失」的实际事故:bool false、空列表、零金额都不上网,下游无法区分「没给」和「给了默认值」,解法是 optional。
Protobuf · Evolution
兼容性设计:字段编号是线上契约,演进规则是铁律
规则(protobuf.dev 原文要求)
- 编号 = 身份,永不可改:编号范围 1~536,870,911;19000~19999 是实现保留段,protoc 直接拒绝使用。
- 加字段:只用全新编号;1~15 的 tag 只要 1 字节,把高频字段放这里。
- 删字段:编号(和名字)用
reserved 封存,防止后来者复用编号 —— 旧客户端把新数据按旧语义读错,是最隐蔽的线上事故。
- wire type 不可变:把 int32 改成 int64 可以(同为 VARINT 且符号扩展兼容),改成 string(LEN)就是灾难。
双向兼容的机制根源
- 前向(新写旧读):旧代码读到未知字段号 → 按 wire type 跳过并保留 → 转发不丢。
- 后向(旧写新读):新代码读不到旧消息里的新字段 → 取默认值。
- 对比 JSON:加字段通常安全,但改名/改类型没有任何机制保护,全靠 code review 纪律;没有「必须跳过未知字段」的强制约定(解析器行为不一),契约退化成文档。
- 对比 Avro:Avro 靠 reader/writer schema 双方解析时匹配,Protobuf 靠编号内嵌 —— 前者 schema 更灵活,后者零额外开销。
// 删字段的正确姿势:编号与名字都进 reserved,一箭双雕防复用
message User {
reserved 2, 15, 9 to 11; // 旧编号封存
reserved "email", "phone"; // 旧名字封存(防 JSON 转换层误用)
int64 id = 1;
string name = 3; // 新字段用全新编号
}
这页是 Protobuf 相对 JSON 最本质的工程优势:它把「向后兼容」从团队纪律升级成编译器和线格式共同强制。reserved 是必考细节——只删字段不 reserve,三个月后有人把编号复用给新类型,旧服务读出新字段按旧类型解释,数据静默损坏。19000~19999 保留段这种数字题也要能答。
Comparison · Bytes
同一消息的字节级对比:JSON 26 B vs Protobuf 11 B
图里每个字节都能手推(08=字段 1 VARINT,96 01=varint 150,12=字段 2 LEN,06=长度 6),面试现场推一遍比背结论有说服力得多。26 对 11 只是无压缩的典型例子,务必主动补上三个边界:gzip 会缩小差距、HPACK 只压 HTTP 头不压 body、真实差距要拿自己的数据测——避免给人「无脑小一半」的错误印象。
Comparison · Matrix
对比总表:同层对齐后再比较
| 维度 | HTTP/1.1 + JSON(Web / REST) | gRPC = HTTP/2 + Protobuf(微服务 RPC) |
| 层次定位 | 传输协议 + 自描述文本序列化 | 传输协议(H2)+ 二进制序列化(proto)+ 调用约定(LPM/Trailers) |
| 报文格式 | ASCII 文本,人眼直读 | 二进制帧 + TLV,需 schema/工具解码 |
| 消息体积 | 字段名与标点重复传输;gzip 可压缩 | 只传编号 + 值,默认值省略;典型明显更小(见字节级对比) |
| 解析成本 | 逐字符词法/语法分析 | 定界跳读 + 整数 tag 比较,生成代码直写 buffer |
| 契约与类型 | 无强制 schema(OpenAPI 是文档约定);动态灵活 | .proto 强契约,编译期生成多语言 stub,类型错误编译期暴露 |
| 兼容机制 | 加字段基本安全;改名/改类型靠纪律 | 编号机制 + reserved + 未知字段保留,演进规则被规范强制 |
| 流式能力 | 1.1 无;H2 有流但无调用级抽象 | 一元 / 服务端流 / 客户端流 / 双向流,第一公民 |
| 连接效率 | 1.1 每连接串行(队头阻塞);实践堆 6 连接 | 长连接多路复用,N 路调用并行交错 |
| 生态工具 | 浏览器 / curl / CDN / 缓存 / 网关全兼容 | 需运行时与 proto 管理;浏览器走 gRPC-Web + 代理;L7 负载均衡需感知 H2 |
| 调试体验 | 抓包即读 | protoc --decode_raw / Protoscope / grpcurl |
| 典型场景 | 对外 API、低频调用、结构多变、可读性优先 | 内部微服务、高 QPS 低延迟、强契约多语言、流式场景 |
这张表按「先对齐层次再逐维比」组织,回答任何对比题都可以从中取行。记忆锚点:三个「机制差」(体积、解析、兼容)都源自同一个根因——JSON 把元数据放运行时,Protobuf 把元数据放编译期;两个「生态差」(工具、调试)则是文本自描述的红利。别把表格背死,理解单向因果即可。
gRPC · over HTTP/2
一次 gRPC 调用的全貌:HTTP/2 帧里的 protobuf
把六步背下来就能画出来:请求头(:method POST + :path 方法名 + application/grpc)→ DATA 帧装 LPM(1 字节压缩标志 + 4 字节大端长度 + protobuf 载荷)→ EOS → 响应头 → 响应 DATA → Trailers 携带 grpc-status。Trailers 是关键差异化:grpc-status 必须在响应体全部发完之后才能确定,HTTP/1.1 的「先头后体」做不到,这就是 gRPC 必须 HTTP/2 的原因之一。gRPC 快的四件事:长连接复用、多路复用、二进制帧、protobuf 编码。
Decision Guide
选型:不是二选一,是分层组合
HTTP + JSON 适合
- 对外 API / 浏览器直连:零运行时依赖,curl、devtools、监控、CDN、网关即开即用。
- 结构多变 / 动态字段:无 schema 编译环节,改字段即改即生效。
- 可读性与调试优先:日志里直接贴 body。
- 低频 / 带宽不敏感:体积与解析优势用不上时,别为它引入契约管理成本。
Protobuf + gRPC 适合
- 内部微服务、高 QPS 低延迟:编解码 + 多路复用的收益随调用量放大。
- 强契约 + 多语言:.proto 是唯一的真相源,CI 里编译期拦截类型不一致。
- 流式需求:双向流是原生能力(消息推送、日志采集、大文件分块)。
- 要接受的成本:schema 仓库与版本管理、gRPC-Web 网关、抓包要解码、L7 负载均衡要感知 H2/Trailers。
常见实践:对外 REST(JSON) + 对内 gRPC(Protobuf),边界处做一次转换;消息队列内部消息体用 Protobuf(Kafka 存 TLV 字节,schema 放 registry)。反模式:拿 Protobuf 存「要人读的」数据(日志、配置导出);用 gRPC 但从来不开流式与多路复用(配了 HTTP/1.1 网关)。
选型题的标准答法是「场景拆分」而不是站队:对外可读兼容优先,对内效率契约优先。主动说出 gRPC 的工程成本(schema 管理、网关/负载均衡要求)会显得有实战经验——性能不是唯一维度,团队规模与运维复杂度同样是决策变量。
Interview QA · 1/2
典型面试 QA(上)
Q1 HTTP 和 Protobuf 是竞争关系吗?
分层gRPC=HTTP/2+Proto
不是。HTTP 是应用层传输协议,Protobuf 是序列化格式,gRPC 是两者的组合。同层对比应是:JSON ↔ Protobuf(编码),HTTP/1.1 ↔ HTTP/2 ↔ HTTP/3(传输)。把层次说对,这道题就已经赢了一半。
Q2 Protobuf 为什么又小又快?
无字段名varint默认值省略TLV 跳读
小:字段编号 1~2 字节替代字段名重复传输;varint 按数值位数变长;proto3 默认值字段整段不上 wire。快:TLV 定界可按 wire type 定长读取或整体跳过,tag 是整数比较;schema 编译期生成代码,无运行时反射(Go 生成代码走快速路径直写 buffer)。
Q3 HTTP/2 彻底解决队头阻塞了吗?
应用层✓TCP 层✗QUIC 流独立
只解决一半。H2 用帧 + Stream 消除了应用层排队,但 TCP 只认字节流:任一报文段丢失,内核必须等重传才能交付其后所有数据,同连接上全部 Stream 一起卡。HTTP/3 把流下沉到 QUIC(用户态、流间独立确认),才在传输层消灭它。
Q4 proto3 默认值不序列化,有什么坑?
0 值消失optionalpresence
标量字段等于默认值(0 / "" / false)时整段不上 wire:「没设置」和「设置成 0」在线上不可区分——bool false、空列表、零金额都会消失。需要区分就必须加 optional(显式 presence,官方推荐 basic types 一律加);消息类型字段天生有 presence,不受影响。
这四题覆盖最高频的追问。Q1 考层次感,Q2 考机制拆解(体积、解析两个维度分开答),Q3 考「解决一半」的辩证表述——能主动说出 TCP 层残余阻塞是加分项,Q4 考实战踩坑。每题的答法都是「先定性、再拆机制、最后给边界」。
Interview QA · 2/2
典型面试 QA(下)
Q5 为什么 int32 存 -1 要 10 字节?怎么办?
符号扩展ZigZagsint32
varint 编码的是补码符号扩展到 64 位后的无符号数,-1 扩展后高位全 1,恒占 10 字节。解法:可能为负的字段用 sint32/sint64——先 ZigZag 变换(p→2p,n→2|n|−1)再 varint,-1 只占 1 字节;确定非负用 int/uint;数值普遍很大用 fixed。
Q6 删字段要注意什么?
reserved编号不复用19000~19999
编号和名字都要进 reserved:只删不封,后人复用编号后,旧客户端会把新字段按旧类型解释,数据静默损坏。同时记住:字段编号上线后永不修改(它是 wire 上的唯一身份);1~15 给高频字段;19000~19999 是实现保留段,protoc 拒绝使用。
Q7 HTTP/1.1 的 pipeline 为什么没被采用?
响应按序重试歧义浏览器未启用
三个原因:响应必须严格按请求序返回,队头阻塞依旧;非幂等方法后 pipeline 出错时无法安全重试(RFC 9112 §9.3.2 明确 SHOULD NOT);收益又被「多开 6 条连接」的简单方案覆盖。结果浏览器从未默认启用,规范保留但事实废弃。
Q8 gRPC 一定比 REST 快吗?
复用+二进制冷连接打平成本另算
不一定。差距来自四个机制(长连接复用、H2 多路复用、二进制帧、protobuf 编码),首次建连与低频调用时差距很小;JSON 开 gzip 后体积可能打平;网关/调试/schema 管理是额外成本。准确说法:高频内网 RPC 场景下端到端开销显著更低。
Q5 是手推题(10 字节 vs 1 字节的对比最有说服力),Q6 考 reserved 的工程动机而非语法,Q7 考「规范存在但事实废弃」的演进视角,Q8 考不站队、讲机制和边界的成熟度。八道 QA 合起来正好覆盖本 deck 的全部主干知识。
References & Related
参考来源 & 相关知识点
相关知识点(点击跳转 · 待沉淀项以虚线标注)
- TCP 三次握手与四次挥手 — HTTP/1.1 与 HTTP/2 的传输底座:握手时延、队头阻塞的 TCP 根源、连接生命周期
- TLS 1.3 与握手优化 待沉淀 — ALPN 协议协商、1-RTT/0-RTT、与 QUIC 握手的融合
- QUIC 内部机制 待沉淀 — 包号/确认/流控、拥塞控制、连接迁移的实现细节
- gRPC-Go 内部实现 待沉淀 — HTTP/2 Transport、编解码层与 protobuf-go 快速路径
本 deck 边界:HTTP 传输协议原理 + Protobuf 线格式与对比。不覆盖:TLS/QUIC 内部、gRPC 框架源码、JSON 解析器实现。
键盘操作:←→ 翻页 · T 换主题 · S 演讲者模式 · O 总览。
所有规范细节对照 RFC 9112/9113/9114/9000、RFC 7541、protobuf.dev 与 gRPC 官方协议文档逐条验证;字节示例(08 96 01 / 26B vs 11B)已用程序核算。版本敏感点(Chrome 106 移除 H2 Push、Go 标准库未内置 HTTP/3)在对应页有标注。