- 后端
- RPC框架
【免费下载链接】grpc-rust
A native gRPC client & server implementation with async/await support.
导读
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 服务,在该层完成两个核心任务:
- 协议翻译:在 gRPC(HTTP/2)与 gRPC-Web(HTTP/1.1 + base64)两种线格式之间做双向转换;
- 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 the
GrpcWebLayerwith 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-webapplication/grpc-web+protoapplication/grpc-web-textapplication/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 setting
accept_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在发出请求时做了三件事:
- 降级协议版本:如果请求是 HTTP/2(tonic 客户端的默认版本),会强制改写为 HTTP/1.1(
*req.version_mut() = Version::HTTP_11); - 改写 Content-Type:把
content-type设置为application/grpc-web; - 包装请求体:把请求 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 的设计边界,避免踩坑:
- 只服务 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 之外的服务语义; - CORS 仅覆盖 gRPC-Web 场景:只处理 gRPC-Web 请求与 gRPC-Web 预检(preflight)请求;
- 流类型限制:当前 gRPC-Web 客户端只能发起unary(一元)和server-streaming(服务端流式)调用,这也是本 crate 唯一设计的处理目标;client-streaming 与双向流式要等浏览器端客户端支持后才会得到官方支持;
- 不支持 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.
相关推荐
Tonic 0.14 入门实战:用 Rust 构建第一个 gRPC HelloWorld 客户端与服务端
Tonic 0.14 入门实战:用 Rust 构建第一个 gRPC HelloWorld 客户端与服务端 本教程以当前仓库 grpc rust 中的官方 Get
后端RPC框架Celery 任务体系基石:深入解析 `celery.app.task` 模块的 Task 基类与 Task.request 请求上下文
Celery 任务体系基石:深入解析 celery.app.task 模块的 Task 基类与 Task.request 请求上下文 本文以 Celery 源码
后端RPC框架如何在Tonic项目中正确配置gRPC客户端HTTPS连接
如何在Tonic项目中正确配置gRPC客户端HTTPS连接 在使用Tonic框架开发gRPC应用时,许多开发者会遇到HTTPS连接失败的问题,特别是当服务部署在
后端RPC框架
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考