news 2026/10/2 2:21:08

使用 tonic-web 为 tonic 服务直接接入 gRPC-Web 客户端:协议翻译、CORS 配置与实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用 tonic-web 为 tonic 服务直接接入 gRPC-Web 客户端:协议翻译、CORS 配置与实战指南
  • 后端
  • RPC框架

【免费下载链接】grpc-rust

A native gRPC client & server implementation with async/await support.

项目地址:https://gitcode.com/GitHub_Trending/to/grpc-rust
点击查看免费下载

导读

tonic-web是 grpc-rust 仓库中负责 gRPC-Web 协议翻译的独立 crate,它能让基于 tonic 构建的 gRPC 服务直接接收来自浏览器端grpc-web客户端的请求,彻底省去 Envoy 等外部代理这一中间环节。读完本文你将掌握:如何在 tonic 服务上启用GrpcWebLayer、如何配置 CORS 与 TLS 场景下的部署差异、如何使用仓库自带的客户端示例发起 gRPC-Web 调用,以及从源码层面理解请求/响应翻译与 trailer 处理的核心原理。

一、为什么需要 gRPC-Web,以及 tonic-web 的定位

原生 gRPC 协议依赖 HTTP/2 的流式语义和 trailer 头部,而浏览器环境下的XMLHttpRequest/fetch无法直接暴露这些能力。传统方案是在服务前面部署 Envoy 之类的代理,把 gRPC-Web 请求翻译成原生 gRPC 请求转发给后端。

tonic-web改变了这一局面:如 tonic-web/README.md 所述,它"Enables tonic servers to handle requests fromgrpc-webclients directly, without the need of an external proxy",即让 tonic 服务器直接处理来自 grpc-web 客户端的请求,无需外部代理。

从 tonic-web/src/lib.rs 的模块文档可以看到它的实现思路:

"It achieves this by wrapping individual tonic services with a tower service that performs the translation between protocols and handles cors requests."

也就是说,它通过一个 tower 服务包装单个 tonic 服务,在该层完成两个核心任务:

  1. 协议翻译:在 gRPC(HTTP/2)与 gRPC-Web(HTTP/1.1 + base64)两种线格式之间做双向转换;
  2. CORS 处理:响应浏览器的跨域预检(preflight)请求。

tonic-web的版本信息与依赖声明见 tonic-web/Cargo.toml(当前版本 0.14.6),其运行依赖包括base64、bytes、tokio-stream、http-body、tonic与tower-service等,其中 base64 引擎正是协议翻译中 text 编码模式的底层支撑。

二、快速开始:让 tonic 服务直接接受 gRPC-Web 请求

2.1 最小启用示例

README 给出的最快上手方式,是在现有的 tonic 服务基础上加一个GrpcWebLayer,并允许服务器接受 HTTP/1.1 请求:

#[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let addr = "[::1]:50051".parse().unwrap(); let greeter = GreeterServer::new(MyGreeter::default()); Server::builder() .accept_http1(true) .layer(GrpcWebLayer::new()) .add_service(greeter) .serve(addr) .await?; Ok(()) }

其中两个关键点缺一不可:

  • GrpcWebLayer::new():把 gRPC-Web 翻译逻辑作为 tower Layer 挂到服务栈上。查看 tonic-web/src/layer.rs 可知它只是一个零成本构造的tower_layer::Layer,layer()方法内部创建GrpcWebService包装内层服务,GrpcWebService同样实现了NamedService,因此服务名称(如helloworld.Greeter)可以无缝透传到路由注册(见 tonic-web/src/service.rs)。
  • accept_http1(true):gRPC-Web 在浏览器里本质上是跑在 HTTP/1.1 之上的,所以必须显式开启 HTTP/1.1 支持,让服务器既能处理 HTTP/2 的原生 gRPC 请求,也能处理 HTTP/1.1 的 gRPC-Web 请求。

2.2 仓库内可运行的完整示例

仓库在 examples/src/grpc-web/server.rs 提供了开箱即用的服务端示例。它与 README 的"最小示例"不同之处在于,把服务构建改成使用tower::ServiceBuilder,并显式叠加了一个 CORS 层:

let greeter = tower::ServiceBuilder::new() .layer(tower_http::cors::CorsLayer::new()) .layer(tonic_web::GrpcWebLayer::new()) .into_inner() .named_layer(GreeterServer::new(greeter)); Server::builder() // GrpcWeb is over http1 so we must enable it. .accept_http1(true) .add_service(greeter) .serve(addr) .await?;

示例中的注释 "GrpcWeb is over http1 so we must enable it" 再次强调:HTTP/1.1 支持是 gRPC-Web 服务化的硬性前提。示例监听127.0.0.1:3000,对应的 protobuf 定义可参考 examples/proto/helloworld/helloworld.proto。

三、自定义 CORS 配置

浏览器端跨域访问 gRPC-Web 服务必然触发 CORS 机制。GrpcWebLayer本身内置了 CORS 处理逻辑(用于 grpc-web 与 grpc-web preflight 请求),但正如 tonic-web/src/lib.rs 所说:

"You can customize the CORS configuration composing theGrpcWebLayerwith the cors layer of your choice."

即你可以将GrpcWebLayer与任意你选择的 CORS layer(例如tower-http的CorsLayer)组合,来自定义允许的来源(origin)、方法、请求头等策略。仓库示例 examples/src/grpc-web/server.rs 中CorsLayer::new()与GrpcWebLayer::new()的叠层组合就是这一能力的标准用法;tonic-web的 dev-dependencies 也声明了对tower-http(开启corsfeature)的依赖(见 tonic-web/Cargo.toml),印证了该组合是官方推荐路径。

需要注意的是,自定义 CORS layer 的适用范围有限:如 tonic-web/src/lib.rs 的限制说明所述,"the cors support implemented by this crate willonlyhandle grpc-web and grpc-web preflight requests",它只为 gRPC-Web 场景服务,不会越权去处理其他任意 HTTP 跨域请求。

何时可以跳过自定义 CORS?

GrpcWebLayer对请求的分类逻辑决定了 CORS 的触发范围。查看 tonic-web/src/service.rs 的RequestKind定义:只有content-type精确命中以下四种之一的请求才会被识别为 gRPC-Web 请求:

  • application/grpc-web
  • application/grpc-web+proto
  • application/grpc-web-text
  • application/grpc-web-text+proto

从源码的匹配分支(tonic-web/src/service.rs)可以看到:命中 gRPC-Web 且方法为POST的请求进入翻译链路;命中但方法不是POST(如 GET/PUT/DELETE 等)会直接返回HTTP 405 Method Not Allowed。这一"只认 POST 和 OPTIONS"的严格行为也被 tonic-web/src/service.rs 的单元测试only_post_and_options_allowed所固化。

四、TLS 服务器场景:可以不开 accept_http1

对于启用了 TLS 的服务器,有一个值得注意的优化点。同样来自 tonic-web/src/lib.rs 的说明:

"Alternatively, if you have a tls enabled server, you could skip settingaccept_http1totrue. This works because the browser will handleALPN."

即当服务器配置了 TLS 时,可以省略accept_http1(true),因为浏览器会通过 ALPN(Application-Layer Protocol Negotiation)在 TLS 握手中自动协商出 HTTP/1.1,从而让 gRPC-Web 流量得以承载。对应的 TLS 版示例代码如下:

#[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let cert = tokio::fs::read("server.pem").await?; let key = tokio::fs::read("server.key").await?; let identity = Identity::from_pem(cert, key); let addr = "[::1]:50051".parse().unwrap(); let greeter = GreeterServer::new(MyGreeter::default()); // No need to enable HTTP/1 Server::builder() .tls_config(ServerTlsConfig::new().identity(identity))? .layer(GrpcWebLayer::new()) .add_service(greeter) .serve(addr) .await?; Ok(()) }

其中Identity与ServerTlsConfig均来自 tonic 的 transport 层。仓库也提供了可直接用于测试的 TLS 证书材料,例如 examples/data/tls/ 目录下的server.pem、server.key、ca.pem以及 interop/data/ca.pem 等。

五、客户端侧:用 GrpcWebClientLayer 发起 gRPC-Web 调用

gRPC-Web 不只是服务端单向的翻译,tonic-web同样提供了客户端支持。核心组件是GrpcWebClientLayer与GrpcWebClientService(导出自 tonic-web/src/lib.rs)。

5.1 仓库示例:通过 hyper 客户端 + GrpcWebClientLayer

examples/src/grpc-web/client.rs 展示了完整用法。由于 gRPC-Web 是 HTTP/1.1 协议,这里不能使用 tonic 默认的 h2 通道,而是直接构造一个 hyper 的 HTTP/1.1 客户端,再叠加GrpcWebClientLayer:

// Must use hyper directly... let client = hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build_http(); let svc = tower::ServiceBuilder::new() .layer(GrpcWebClientLayer::new()) .service(client); let mut client = GreeterClient::with_origin(svc, "http://127.0.0.1:3000".try_into()?); let request = tonic::Request::new(HelloRequest { name: "Tonic".into(), }); let response = client.say_hello(request).await?; println!("RESPONSE={response:?}");

要点解析:

  • 客户端必须基于 HTTP/1.1 的 hyper 客户端(示例注释 "Must use hyper directly...");
  • GreeterClient::with_origin用于指定目标地址(http://127.0.0.1:3000),与前面服务端示例的监听地址一一对应;
  • 生成代码来自tonic::include_proto!("helloworld"),对应的预生成文件可参考 examples/generated/helloworld/ 目录。

5.2 客户端翻译的底层动作

查看 tonic-web/src/client.rs 可以看到GrpcWebClientService在发出请求时做了三件事:

  1. 降级协议版本:如果请求是 HTTP/2(tonic 客户端的默认版本),会强制改写为 HTTP/1.1(*req.version_mut() = Version::HTTP_11);
  2. 改写 Content-Type:把content-type设置为application/grpc-web;
  3. 包装请求体:把请求 body 用GrpcWebCall::client_request包装,进入编码方向(Direction::Encode)。

响应侧则用GrpcWebCall::client_response反向包装(进入Direction::Decode),负责把服务端返回的 gRPC-Web 帧还原为 tonic 客户端能消费的数据流(见 tonic-web/src/client.rs)。这样客户端业务代码完全无感,仍以普通 tonic 客户端的方式编写调用。

六、深入原理:请求/响应的翻译是怎么完成的

这一节从源码结构出发,剖析tonic-web的翻译管线。它本质上是一个tower 服务栈 + HTTP body 适配器的组合。

6.1 请求分类:GrpcWebService 的路由决策

GrpcWebService::call(tonic-web/src/service.rs)首先对入站请求做分类,决策逻辑如下表:

条件处理方式
content-type 命中 4 种 gRPC-Web 类型之一,且方法为POST翻译为 gRPC 请求转发给内层服务,再把响应翻译回 gRPC-Web
content-type 命中 gRPC-Web 类型,但方法非POST直接返回HTTP 405
非 gRPC-Web 请求,但版本为 HTTP/2原样透传给内层服务(保持原生 gRPC 兼容)
其他请求(如 HTTP/1.1 的非 gRPC-Web 请求)返回HTTP 400

值得说明的是"其他 HTTP/2 请求透传"这一设计:它保证了原生 gRPC 客户端(HTTP/2)与 gRPC-Web 客户端(HTTP/1.1)可以共用同一个服务实例,这正是"一层挂载、双协议兼容"的架构红利。对应行为由 tonic-web/src/service.rs 的mod grpc测试验证:HTTP/2 的application/grpc请求返回 OK,而 HTTP/1.1 的application/grpc请求返回 400。

6.2 请求头改写:coerce_request

对于命中的 gRPC-Web 请求,coerce_request(tonic-web/src/service.rs)会把 HTTP/1.1 的 gRPC-Web 请求头改写成符合 gRPC 规范的请求头:

  • 移除content-length(gRPC 用帧长度而非 HTTP 头表达消息长度);
  • 设置content-type: application/grpc,让内层 tonic 服务把它当作普通 gRPC 请求;
  • 插入te: trailers,声明客户端可接收 trailer;
  • 设置accept-encoding: identity,deflate,gzip,与 tonic 的压缩协商保持一致。

随后请求 body 被GrpcWebCall::request(...)包装,进入解码方向(把 gRPC-Web 的 base64 载荷还原为 gRPC 二进制帧)。

6.3 帧格式与编码:GrpcWebCall

GrpcWebCall<B>(tonic-web/src/call.rs)是一个同时实现http_body::Body与Stream的适配器,负责在字节流层面做转换。几个关键常量定义了 gRPC-Web 的线格式:

const GRPC_HEADER_SIZE: usize = 1 + 4; // 帧头:1 字节 flag + 4 字节消息长度 const FRAME_HEADER_SIZE: usize = 5; const GRPC_WEB_TRAILERS_BIT: u8 = 0b10000000; // 帧首字节 MSB 置位表示 trailer 帧 const BUFFER_SIZE: usize = 8 * 1024; // base64 缓冲大小

核心要点包括:

  • 编码选择:Encoding枚举只有两种——None(二进制,对应application/grpc-web(+proto))与Base64(对应application/grpc-web-text(+proto))。Encoding::from_content_type/from_accept通过请求头决定编码方式,响应头的 content-type 也据此回写(见 tonic-web/src/call.rs)。从服务端看,text 变体要求接受端同时支持,因此响应编码以Accept头为准;
  • Trailer 的"帧化":原生 gRPC 用 HTTP/2 trailer 传递grpc-status、grpc-message等状态元数据,而 HTTP/1.1 没有 trailer 概念。tonic-web的做法是把 trailer 编码成 HTTP/1 头块格式(key: value\r\n)塞进一个特殊的 gRPC 数据帧——首字节置位GRPC_WEB_TRAILERS_BIT,后面跟长度和 trailer 内容(见make_trailers_frame,tonic-web/src/call.rs)。这一约定是浏览器端 gRPC-Web 客户端的标准协议,也是本 crate 处理 trailer 的核心;
  • 解码侧 trailer 识别:服务端解码请求、以及客户端解码响应时,通过find_trailers(tonic-web/src/call.rs)逐帧扫描字节流:按"1 字节 flag + 4 字节长度"的帧头步进,一旦遇到0b10000000位帧头即认定 trailer 帧开始;若剩余字节不足以构成完整帧则判定缓冲区不完整(IncompleteBuf),等待更多数据到达再继续。decode_trailers_frame则把 trailer 帧内容解析回HeaderMap,并兼容key: value(冒号后有空格)与key:value两种写法——后者在 tonic-web/src/call.rs 的测试中明确说明是为了兼容 connect-rpc 与标准 HTTP 的书写习惯;
  • base64 的分块解码:decode_chunk按 4 的倍数切分缓冲(max_decodable),避免把不完整的分组交给 base64 解码器,同时处理了"以 base64 编码的 trailer"这类边界情况。

6.4 状态与错误

转换过程中出现的任何解析失败都会被归一化为Status::internal(format!("tonic-web: {e}"))(tonic-web/src/call.rs),例如非法帧位(Invalid header bit {header} expected 0 or 1)、畸形 base64、无法解析的 trailer 键值对等。这使得 gRPC 生态的错误传播模型在翻译层保持一致,上层业务无需感知协议差异。

七、能力边界与限制

阅读 tonic-web/src/lib.rs 的Limitations一节,可以明确本 crate 的设计边界,避免踩坑:

  1. 只服务 gRPC-Web 兼容客户端:tonic-web专为 gRPC-Web 客户端设计,"It is not expected to handle arbitrary HTTP/x.x requests or bespoke protocols"。虽然 HTTP/2 的原生 gRPC 请求会透传,但任意 HTTP 请求并不会得到 gRPC 之外的服务语义;
  2. CORS 仅覆盖 gRPC-Web 场景:只处理 gRPC-Web 请求与 gRPC-Web 预检(preflight)请求;
  3. 流类型限制:当前 gRPC-Web 客户端只能发起unary(一元)和server-streaming(服务端流式)调用,这也是本 crate 唯一设计的处理目标;client-streaming 与双向流式要等浏览器端客户端支持后才会得到官方支持;
  4. 不支持 WebSocket 传输:gRPC-Web 的 WebSocket transport 变体不在支持范围内。

这些限制意味着:如果你的服务大量使用双向流式接口,且必须直接从浏览器访问,那么仍需评估代理方案或等待 gRPC-Web 生态的后续演进。

八、验证路径:测试与可运行示例

仓库为tonic-web提供了多层次的验证材料,供读者自行对照实验:

  • 单元测试:集中在 tonic-web/src/service.rs 与 tonic-web/src/call.rs。前者覆盖请求分类(4 种 content-type、非 POST 返回 405、原生 gRPC 透传、任意 HTTP/1.1 返回 400、带/不带 origin 的 CORS 预检等),后者覆盖 trailer 编解码(含多 trailer、值内冒号、冒号后空格、缓冲不完整等多种边界);
  • 集成测试:仓库在 tests/web/tests/grpc.rs 与 tests/web/tests/grpc_web.rs 提供端到端验证,对应测试 proto 为 tests/web/proto/test.proto;
  • 运行示例:cd examples && cargo run --bin grpc-web-server与cargo run --bin grpc-web-client可分别启动服务端与客户端(示例源码见 examples/src/grpc-web/server.rs 与 examples/src/grpc-web/client.rs)。

结语

tonic-web用约两千行的精简实现,把"浏览器直连 gRPC 服务"从需要外部代理的繁琐部署,收敛为一个 tower Layer 的挂载操作:服务端GrpcWebLayer + accept_http1(true)一行开启翻译与 CORS,客户端GrpcWebClientLayer一行完成协议适配;底层则由GrpcWebCall在帧级别完成 base64 编解码与 trailer 帧化。配合本仓库的完整示例与测试用例,你可以快速搭建并验证一条"浏览器 → gRPC-Web (HTTP/1.1) → tonic 服务"的端到端链路,并在此基础上继续扩展 CORS 策略、接入 TLS,或深入定制协议行为。

  • 后端
  • RPC框架

【免费下载链接】grpc-rust

A native gRPC client & server implementation with async/await support.

项目地址:https://gitcode.com/GitHub_Trending/to/grpc-rust
点击查看免费下载
上一篇:Velero `restore delete` 命令完全指南:从 CLI 用法到源码级删除流程解析
下一篇:Zephyr RTOS 上的 ESP32-C3-DevKitC 开发板:从构建烧录到 QEMU 仿真与调试的完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 2:20:48

Python ag-funutils 包详解与实战案例

1. 引言ag-funutils 是一个面向 Python 的函数式编程工具包&#xff0c;旨在为开发者提供一组轻量、易用且可组合的工具函数&#xff0c;帮助简化日常开发中的数据处理、集合操作和函数组合等任务。它借鉴了函数式编程语言中的常用模式&#xff0c;同时保持了 Python 的简洁风格…

作者头像 李华
网站建设 2026/10/2 2:20:48

5分钟上手Label Studio:多模态数据标注完全指南

5分钟上手Label Studio&#xff1a;多模态数据标注完全指南 【免费下载链接】label-studio Label Studio is a multi-type data labeling and annotation tool with standardized output format 项目地址: https://gitcode.com/GitHub_Trending/la/label-studio 标注队列…

作者头像 李华
网站建设 2026/10/2 2:20:00

Windows下安装Redis的四种方式与配置排查指南

在Linux上装Redis&#xff0c;一句apt install redis-server就完事&#xff0c;连配置文件都不用动。到了Windows就完全是另一番景象&#xff1a;你去Redis官网的Download页翻一遍&#xff0c;Linux、macOS、Docker的安装说明都列得清清楚楚&#xff0c;唯独没有Windows安装包。…

作者头像 李华
网站建设 2026/10/2 2:16:03

ArcGIS矢量化与拓扑检查:从扫描图到干净SHP的完整链路

简介&#xff1a;这份资源面向GIS初学者与测绘、规划、自然资源等行业的从业者&#xff0c;围绕ArcGIS矢量化与拓扑检查两大核心技能&#xff0c;提供一套可直接上手练习的完整数据包&#xff0c;帮助解决栅格转矢量、空间关系校验与数据质量修复等实际问题。压缩包共37个文件&…

作者头像 李华
网站建设 2026/10/2 2:14:29

具身智能中的协同机理(11):TVA-VLA边缘端推理与强光降噪研究

前沿技术探索&#xff1a;TVA智能体&#xff08;简称TVA&#xff09;TVA智能体&#xff08;亦称“AI智能体视觉”&#xff09;是依托Transformer架构与“因式智能体”理论构建的新型工业视觉系统&#xff0c;也是当前最具代表性的具身视觉技术之一。它有机融合深度强化学习&…

作者头像 李华