首页
/ gRPC-Web 协议详解:gRPC 浏览器传输协议与原生 gRPC over HTTP/2 的差异分析

gRPC-Web 协议详解:gRPC 浏览器传输协议与原生 gRPC over HTTP/2 的差异分析

2026-09-05 14:07:35作者:贡沫苏Truman

本文基于 gRPC 仓库中的 gRPC-Web 协议文档,完整解读 gRPC-Web 传输协议的设计目标、与原生 gRPC over HTTP/2 协议 的全部差异点,包括 Content-Type 协商、消息分帧、trailers 内嵌于响应体的编码方式、文本流编码规则,以及重试、缓存、keep-alive 等特性的协议现状。读完本文,你将能够准确理解 gRPC-Web 请求/响应在浏览器与代理(如 Envoy、nginx 网关)之间的线上格式,并能为 gRPC-Web 的代理层实现或调试工作提供协议依据。

协议定位:为浏览器而生的“delta”协议

gRPC-Web 提供 JS 客户端库,支持与 gRPC-Node 相同的 API 来访问 gRPC 服务。但由于浏览器的限制,Web 客户端库实现的并不是原生的 gRPC 协议(即 PROTOCOL-HTTP2.md 所定义的协议),而是一个不同的协议。该协议在设计上刻意降低了实现复杂度,使得一个代理(proxy)能够方便地在两种协议之间做转换——这正是 gRPC-Web 最可能的部署形态:浏览器通过 HTTP 与 gRPC-Web 代理对话,代理再将请求翻译为原生 gRPC(HTTP/2)发给后端服务。

需要特别注意的是原文档的表述方式:本文档描述的是一种“delta”(增量差异),即 gRPC-Web 协议相对原生 gRPC over HTTP/2 协议细节的差异清单,而非一份自包含的完整协议规范。因此阅读 gRPC-Web 协议时,必须同时对照 gRPC over HTTP/2 协议文档,两者结合才能构成完整的协议视图。

设计目标

根据 PROTOCOL-WEB.md 的 Design goals 一节,gRPC-Web 协议确定了以下三条设计目标:

  1. 尽可能沿用 "application/grpc" 的分帧方式(framing):保证与原生 gRPC 的最大兼容性,降低代理转译的实现成本;
  2. 与 HTTP/2 分帧解耦:因为浏览器现在、也(在协议制定时)永远不会直接暴露 HTTP/2 分帧给 JavaScript 层;
  3. 支持文本流(如 base64):以提供跨浏览器支持(文档中明确提到的目标是覆盖 IE-10 这类不直接支持二进制流的浏览器环境)。

原文档还给出了该协议的两个长期演进预期:

  • gRPC-Web 协议会持续演化,主要为浏览器客户端优化,或支持 Web 特有特性(如 CORS、XSRF);
  • 当浏览器能够通过新的 whatwg streams API 直接说原生 gRPC 协议后,这个协议在 1-2 年内会变得可选(become optional)。原文档同时强调:该协议会公开发布/评审,但 gRPC-Web 方面也意图将其保留为 gRPC-Web 的内部细节(internal detail),即它不是要求所有实现都必须永久遵守的强约束。

与 gRPC over HTTP/2 的协议差异

下面按 PROTOCOL-WEB.md 的章节顺序,完整列出并逐条讲解各差异点。

Content-Type 差异

原生 gRPC 使用 application/grpc(可选 +proto / +json 后缀);gRPC-Web 则使用两种 Content-Type:

  1. application/grpc-web
    • 例如 application/grpc-web+protoapplication/grpc-web+jsonapplication/grpc-web+thrift
    • 发送方应始终指明消息格式+proto+json 等后缀);
    • 接收方在 Content-Type 为裸的 application/grpc-web(缺失消息格式后缀)时,应假定默认为 +proto
  2. application/grpc-web-text
    • 语义为 "application/grpc-web 的文本编码流",例如 application/grpc-web-text+protoapplication/grpc-web-text+thrift

这一差异的意义在于:代理层只需通过 Content-Type 前缀即可识别 gRPC-Web 流量,而 text 变体让代理知道响应体需要做 base64 文本化后再发往浏览器。

HTTP 线上协议(wire protocol)差异

  1. 支持任意 HTTP/*:不依赖任何 HTTP/2 特有的分帧机制。也就是说 gRPC-Web 请求可以跑在 HTTP/1.1 之上,这直接决定了它能在不暴露 HTTP/2 的浏览器环境中工作;
  2. 头部大小写规则:在 HTTP/1.1 上传输时,头部名可以为大写或混合大小写;但编码在最后一个长度前缀消息(length-prefixed message)里的 trailers 必须始终使用小写名称。这是因为 trailers 在 gRPC-Web 中是"伪装成 HTTP/1 头块"嵌在 body 里传输的(见下文分帧一节),接收端按小写 HTTP 头解析;
  3. 使用 EOF(请求体/响应体的结束)来关闭流,替代原生 gRPC 中依赖 HTTP/2 END_STREAM 帧标志位的做法。

HTTP/2 相关行为的缺失

相对 PROTOCOL-HTTP2.md 中规定的 HTTP/2 行为,gRPC-Web 明确不支持也不使用

  1. stream-id:不存在多路复用的流标识。这意味着单条 HTTP 连接上的请求/响应处理退化为 HTTP/1 语义,多路复用交给浏览器与代理层(如 HTTP/2 代理)负责;
  2. GOAWAY 帧:不使用。优雅关闭依赖 HTTP 层自身的连接管理。

消息分帧:状态与 trailers 移入响应体

这是 gRPC-Web 相对原生协议最核心的结构性变化。在原生 gRPC over HTTP/2 中,响应由 Response-Headers、若干 Length-Prefixed-Message 和独立的 Trailers 帧组成(参见 PROTOCOL-HTTP2.md 的 Response 一节);而 gRPC-Web 中:

  1. 响应状态编码进响应体:gRPC 状态(grpc-status 等)不再作为独立的 trailers 帧,而是以"一个 HTTP/1 头部块(不带终止空行)的键值对形式"编码在响应 body 中,头块格式遵循 RFC 7230 Section 3.2,例如:

    key1: foo\r\n
    key2: bar\r\n
    

    代理转译时,等价于把原生协议的 Trailers 帧内容序列化成这段"文本头块"塞进 body;

  2. 首帧标志位的复用:gRPC 长度前缀消息的第一字节最高位(MSB)原本表示压缩标志(compressed flag,见 PROTOCOL-HTTP2.md 的 Length-Prefixed-Message 定义),gRPC-Web 将其重新解释为:

    • 0:data(数据消息)
    • 1:trailers(trailer 消息,作为 body 的一部分发送)

    文档给出的位图示例:

    10000000b: an uncompressed trailer (as part of the body)
    10000001b: a compressed trailer
    

    10000000b(0x80)表示一个未压缩的 trailer,10000001b(0x81)表示压缩的 trailer;

  3. trailers 必须是响应的最后一条消息,由实现强制保证(enforced by the implementation);

  4. Trailers-only 响应:相对 gRPC 协议规范无变化。trailers 可以与响应头一起发送,body 中没有任何消息——例如调用在发送数据前就立即出错时,代理直接把带 gRPC 状态的头块作为 trailers-only 响应返回。

这条设计目标第 1 条("尽可能沿用 application/grpc 分帧")在这里体现得很彻底:消息体的 5 字节头(1 字节标志 + 4 字节大端长度)保持不变,只是把标志位的语义从"压缩"扩展为"trailers/压缩 trailers",并借 body 尾部的文本头块替代了 HTTP/2 的 trailers 帧。

User-Agent 头

  • 不要使用 User-Agent——该头由浏览器默认设置,客户端库不应自行设置;
  • 改用 X-User-Agent: grpc-web-javascript/0.1,格式与 gRPC over HTTP/2 协议 中规定的 user-agent 结构化字符串格式保持一致。

这个约定让服务端可以从头字段识别出请求来自 gRPC-Web 客户端而非原生客户端,从而在日志、监控和特性协商上做出区分。

文本编码流(base64)

为覆盖不支持二进制流的浏览器/接口(例如 XHR 受安全策略限制只能处理文本),协议定义了文本编码的响应流:

  1. 客户端通过 Accept 头声明需要文本编码,例如在使用 XHR 或受 XHR 安全策略限制时:

    Accept: application/grpc-web-text
    
  2. 默认文本编码是 base64。文档对此有两点重要的实现约束:

    • 不应使用 Content-Transfer-Encoding: base64。原因是 gRPC 消息以帧为单位做 base64 时,每个消息块自身会带 base64 填充(padding),导致整个响应体不一定构成一个合法的单一 base64 实体——即不能把 body 当成一整段 base64 解码;
    • 虽然服务端运行时总是按消息原子地(atomically)做 base64 编码并刷新(flush),但客户端库不能假设 base64 填充一定发生在消息帧边界处。实现可能在运行时每次需要刷新字节缓冲时,就发送一段带有潜在填充的 base64 "chunks"。客户端解码器必须逐 chunk 处理并拼接,而不是等到 body 结束才整体解码。

这段描述是 gRPC-Web 客户端与代理实现中最容易踩坑的部分:base64 流是"按 flush 分片、片内可能带 padding"的流式编码,而非整体编码。

其他特性的协议现状

PROTOCOL-WEB.md 的 Other features 一节还给出了一批特性的状态说明,逐条整理如下:

  • 重试与缓存(Retries, caching):待 gRPC 规范中对应扩展定型后再制定 gRPC-Web 的细则。方向上:安全重试用 PUT(Safe retries: PUT);缓存考虑对头部编码请求的支持,或一个 Web 专属规范。
  • Keep-alive:不支持也不使用 HTTP/2 PING;也不支持 send-beacon(GET 方式的心跳)。长连接保活依赖代理与浏览器自身的 HTTP 机制。
  • 双向流(Bidi-streaming,带流控):当时取决于 whatwg fetch/streams 规范定型并在现代浏览器落地;文档给出的方向是——一旦现代浏览器支持,gRPC-Web 客户端将直接支持原生 gRPC 协议,而不再依赖该 delta 协议做双向流。
  • 版本化(Versioning):未来可能引入特殊头来支持会破坏兼容性的新特性,即通过协议头进行特性/版本协商。
  • 浏览器专属特性:对浏览器或 HTML 客户端独有的特性,原文档指向 grpc-web 仓库(grpc/grpc-web)中发布的浏览器特性规范文档(browser-features.md),该文档不在本仓库内,这里仅作为协议文档的指引记录。

在 gRPC 仓库中的佐证与配套材料

从本仓库的结构看,gRPC-Web 协议本身不落在 gRPC 核心库(C++/Python 等运行时)的实现范围内——application/grpc-web 这一 Content-Type 在整个仓库中仅出现于 doc/PROTOCOL-WEB.md 本身,这与原文档"协议保留为 gRPC-Web 内部细节"的定位一致:核心仓库负责原生 gRPC 协议的规范与实现,gRPC-Web 客户端与代理转译由独立的 grpc-web 仓库承担。

仓库内可以对照参考的材料包括:

  • examples/php/echo/README.md:端到端 PHP 示例。其中"Run the Server"一节给出了通过 grpc-web 仓库构建 grpcweb/node-server 镜像并暴露 9090 端口的步骤,可作为观察 gRPC-Web 后端服务形态的实操入口(该示例中的 gRPC 服务通过 grpc-web 提供的 Node 代理对外提供);
  • examples/node/README.md:说明 Node 相关示例已迁移至 grpc/grpc-node 仓库——gRPC-Web 的 JS 客户端库同样属于 grpc-node / grpc-web 生态而非本核心仓库;
  • doc/PROTOCOL-HTTP2.md:gRPC-Web 所有 delta 描述的基线协议,其中 Length-Prefixed-MessageCompressed-FlagTrailers 等 ABNF 定义是理解 gRPC-Web 分帧差异的前置知识,建议配合阅读。

小结

gRPC-Web 协议是一份以"delta"方式定义的传输规范,其核心思想可归纳为三点:

  1. 分帧尽量不变——沿用 application/grpc 的长度前缀消息格式,仅把首字节标志位的含义扩展为 data/trailers,最大限度降低代理转译成本;
  2. HTTP 层全面降级兼容——支持任意 HTTP/*、用 EOF 关闭流、去掉 stream-id 与 GOAWAY,将 gRPC 状态和 trailers 以文本头块形式内嵌进响应体;
  3. 面向浏览器约束做文本化——通过 Accept: application/grpc-web-text 协商 base64 文本流,并明确以"按 flush 分片的 base64"语义约束客户端解码。

对于需要实现或调试 gRPC-Web 代理转译的工程师,本文覆盖的 Content-Type 判定、trailers 内嵌编码(0x80/0x81 标志)、trailers 小写要求、EOF 关流与 base64 分片规则,构成了一份可直接对照检查的协议要点清单。

登录后查看全文
热门项目推荐
相关项目推荐