十八、Rust gRPC 多 proto 演示
网上及各官方资料,基本是一个 proto 文件,而实际项目,大多是有层级结构的多 proto 文件形式,本篇文章 基于此诉求,构建一个使用多 proto 文件的 rust grpc 使用示例。
关于 grpc 的实现,找到两个库:
Tonic:https://github.com/hyperium/tonic,8.9k Star、852 Commits、2024-03-12 updated。
PingCAP 的 grpc-rs:https://github.com/tikv/grpc-rs,1.8k Star、357 Commits、2023-08 updated。
据说 grpc-rs 的 benchmark 稍高一些,但看关注度和提交量不如 tonic,且据说 tonic 开发体验更好一些,本篇以 tonic 为例。
编译 Protobuf,还需要 protoc,可以参考官方文档,这里先给出 macOS 的:
brew install protobuf
- https://grpc.io/docs/protoc-installation/
关于 Tonic
:Tonic 是基于 HTTP/2 的 gRPC 实现,专注于高性能,互通性和灵活性;
目录说明
.
├── Cargo.toml
├── README.md
├── build.rs
├── proto
│ ├── basic
│ │ └── basic.proto
│ ├── goodbye.proto
│ └── hello.proto
└── src
├── bin
│ ├── client.rs
│ └── server.rs
├── lib.rs
└── proto-gen
├── basic.rs
├── goodbye.rs
└── hello.rs
build.rs
存放通过 proto 生成 rs 的脚本;proto
目录放置 grpc 的 proto 文件,定义服务和消息体;src
常规意义上的项目源码目录;proto-gen
目录存放build.rs
编译 proto 后生成的 rs 文件;lib.rs
引入 proto 的 rs 文件;bin
目录下进行 proto 所定义服务的实现,此例为 客户端、服务端 的实现;
创建项目
- 首先创建一个 lib 项目:
cargo new grpc --lib
并按前面的说明,在 src/bin 目录下,进行 client 及 server 的实现。
Cargo.toml
[package]
name = "grpc"
version = "0.1.0"
edition = "2021"
description = "A demo to learn grpc with tonic."
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name="server"
path="src/bin/server.rs"
[[bin]]
name="client"
path="src/bin/client.rs"
[dependencies]
prost = "0.12.3"
tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"] }
tonic = "0.11.0"
[build-dependencies]
tonic-build = "0.11.0"
定义服务
- grpc/proto/basic/basic.proto
syntax = "proto3";
package basic;
message BaseResponse {
string message = 1;
int32 code = 2;
}
- grpc/proto/hello.proto
syntax = "proto3";
import "basic/basic.proto";
package hello;
service Hello {
rpc Hello(HelloRequest) returns (HelloResponse) {}
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string data = 1;
basic.BaseResponse message = 2;
}
- grpc/proto/goodbye.proto
syntax = "proto3";
import "basic/basic.proto";
package goodbye;
service Goodbye {
rpc Goodbye(GoodbyeRequest) returns (GoodbyeResponse) {}
}
message GoodbyeRequest {
string name = 1;
}
message GoodbyeResponse {
string data = 1;
basic.BaseResponse message = 2;
}
配置编译
Rust 约定:在 build.rs
中定义的代码,会在编译真正项目代码前被执行,因此 可以在这里先编译 protobuf 文件;
- grpc/Cargo.toml 引入
[build-dependencies]
tonic-build = "0.11.0"
- grpc/build.rs
use std::error::Error;
use std::fs;
static OUT_DIR: &str = "src/proto-gen";
fn main() -> Result<(), Box<dyn Error>> {
let protos = [
"proto/basic/basic.proto",
"proto/hello.proto",
"proto/goodbye.proto",
];
fs::create_dir_all(OUT_DIR).unwrap();
tonic_build::configure()
.build_server(true)
.out_dir(OUT_DIR)
.compile(&protos, &["proto/"])?;
rerun(&protos);
Ok(())
}
fn rerun(proto_files: &[&str]) {
for proto_file in proto_files {
println!("cargo:rerun-if-changed={}", proto_file);
}
}
稍作解释:
OUT_DIR
全局定义 proto 文件编译后的输出位置(默认在target/build
目录下)。let protos = [...]
声明了所有待编译 proto 文件。tonic_build::configure()
.build_server(true)
是否编译 server 端,项目以 proto 为基准,则编就完了。.compile(&protos, &["proto/"])?;
开始编译。
最终生成:
- grpc/src/proto-gen/
- basic.rs、hello.rs、goodbye.rs
由 proto 生成的原代码,内容一般较长,这里不贴出,感兴趣的读者,运行一下就可以看到。另外翻看其代码,可以看到:
- 为客户端生成的
HelloClient
类型:impl<T> HelloClient<T>
实现了Clone
、Sync
及Send
,因此可以跨线程使用。 - 为服务端生成的
HelloServer
类型:impl<T: Hello> HelloServer<T> {}
包含了impl<T: Hello>
,预示着我们创建HelloServer
实现,假设为HelloService
时,需实现该Hello Trait
。
引入proto生成的文件
- grpc/src/lib.rs
#![allow(clippy::derive_partial_eq_without_eq)]
pub mod basic {
include!("./proto-gen/basic.rs");
}
pub mod hello {
include!("./proto-gen/hello.rs");
}
pub mod goodbye {
include!("./proto-gen/goodbye.rs");
}
这里使用了标准库提供的
include!
来引入源文件;如果没有定义 proto 编译输出位置的话,默认是在
target/build
目录下,此时需要使用 tonic 提供的include_proto!("hello")
宏,来引入对应文件,而不用额外提供路径了,其中的hello
为 grpc 的 “包名”,具体来说就是:- 注释掉
grpc/build.rs
中.out_dir(OUT_DIR)
一行。 grpc/src/lib.rs
中:include!("./proto-gen/basic.rs");
改为include_proto!("basic");
。include!("./proto-gen/hello.rs");
改为include_proto!("hello");
。include!("./proto-gen/goodbye.rs");
改为include_proto!("goodbye");
。
- 但这样,在进行 server、client 实现、源码编写时,将无法正常引用,致使大量 “漂红” (只 IDE 下这样,如 CLion,不影响 shell 下编译及运行) 。
- 注释掉
参考官方文档:https://docs.rs/tonic/latest/tonic/macro.include_proto.html
服务实现
服务端实现各语言基本类似,为对应 proto 定义,创建相应的 Service 实现即可:
- grpc/src/bin/server.rs
use tonic::{Request, Response, Status};
use tonic::transport::Server;
use grpc::basic::BaseResponse;
use grpc::goodbye::{GoodbyeRequest, GoodbyeResponse};
use grpc::goodbye::goodbye_server::{Goodbye, GoodbyeServer};
use grpc::hello;
use hello::{HelloRequest, HelloResponse};
use hello::hello_server::{Hello, HelloServer};
#[derive(Default)]
pub struct HelloService {}
#[tonic::async_trait]
impl Hello for HelloService {
async fn hello(&self, req: Request<HelloRequest>) -> Result<Response<HelloResponse>, Status> {
println!("hello receive request: {:?}", req);
let response = HelloResponse {
data: format!("Hello, {}", req.into_inner().name),
message: Some(BaseResponse {
message: "Ok".to_string(),
code: 200,
}),
};
Ok(Response::new(response))
}
}
#[derive(Default)]
pub struct GoodbyeService {}
#[tonic::async_trait]
impl Goodbye for GoodbyeService {
async fn goodbye(
&self,
req: Request<GoodbyeRequest>,
) -> Result<Response<GoodbyeResponse>, Status> {
println!("goodbye receive request: {:?}", req);
let response = GoodbyeResponse {
data: format!("Goodbye, {}", req.into_inner().name),
message: Some(BaseResponse {
message: "Ok".to_string(),
code: 200,
}),
};
Ok(Response::new(response))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "0.0.0.0:50051".parse()?;
println!("server starting at: {}", addr);
Server::builder()
.add_service(HelloServer::new(HelloService::default()))
.add_service(GoodbyeServer::new(GoodbyeService::default()))
.serve(addr)
.await?;
Ok(())
}
- grpc/src/bin/client.rs
use tonic::Request;
use tonic::transport::Endpoint;
use grpc::goodbye::goodbye_client::GoodbyeClient;
use grpc::goodbye::GoodbyeRequest;
use grpc::hello;
use hello::hello_client::HelloClient;
use hello::HelloRequest;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = Endpoint::from_static("https://127.0.0.1:50051");
let mut hello_cli = HelloClient::connect(addr.clone()).await?;
let request = Request::new(HelloRequest {
name: "tonic".to_string(),
});
let response = hello_cli.hello(request).await?;
println!("hello response: {:?}", response.into_inner());
let mut goodbye_cli = GoodbyeClient::connect(addr).await?;
let request = Request::new(GoodbyeRequest {
name: "tonic".to_string(),
});
let response = goodbye_cli.goodbye(request).await?;
println!("goodbye response: {:?}", response.into_inner());
Ok(())
}
运行及测试
cargo run --bin server
cargo run --bin client
故障时重新编译:
cargo clean && cargo build
关于 Github Action
- 需添加步骤
- name: Install protoc
run: sudo apt-get install -y protobuf-compiler
完事 ~~
参考资料:
Rust grpc 实现 - https://jasonkayzk.github.io/2022/12/03/Rust%E7%9A%84GRPC%E5%AE%9E%E7%8E%B0Tonic/
Tonic 流式 grpc - https://github.com/hyperium/tonic/blob/master/examples/routeguide-tutorial.md
开源库 - https://github.com/tokio-rs/prost
Tonic - https://github.com/hyperium/tonic