gRPC Stub 是一个作为 gRPC 客户端接口的模块。您可以使用这些存根执行多项操作,例如通过流式或非流式表示法连接和交换数据。 protobuf 编译器为指定语言生成源代码,而该源代码包含所有存根。您可以将生成的源代码导入客户端和服务器端,以通过遵循接口中定义的合同来实现业务逻辑。
创建项目
初始化配置 gRPC 项目
打开命令行终端,依次输入以下指令创建 gRPC 项目:
cargo new tonic-getting
cd tonic-getting
# 若找不到 cargo `add` 命令请安装:`cargo install cargo-edit`。
cargo add tonic prost anyhow
cargo add --build tonic-build
创建 proto 文件
创建 proto 目录存放 protocol buffers 文件定义。添加以下内容到文件 proto/getting/v1/auth.proto 中:
syntax = "proto3";
package getting.v1;
service AuthService {
rpc Signin(SigninRequest) returns (SigninResponse) {}
rpc Signup(SignupRequest) returns (SignupResponse) {}
}
enum TokenType {
TOKEN_TYPE_UNSPECIFIED = 0;
TOKEN_TYPE_BEARER = 1;
}
message SigninRequest {
string email = 1;
string password = 2;
}
message SigninResponse {
string access_token = 1;
TokenType token_type = 2;
}
message SignupRequest {
string email = 1;
string password = 2;
}
message SignupResponse {
}
使用 build.rs 自动化编译 proto 存根代码
创建 build.rs 文件自动编译 *.proto 并生成 Rust 存根代码。
fn main() {
println!("cargo::rerun-if-changed=proto/getting/**/*");
tonic_build::configure()
.compile(&["proto/getting/v1/auth.proto"], &["proto"])
.unwrap();
}
正确完成这些步骤后,cargo 将自动编译 .proto 文件并生成 Rust 存根代码,生成的代码默认并存放在 target/debug/build/tonic-getting-bbf494a1a6da115c/out/getting.v1.rs(注:你看到的具体的路径可能不一样,注意16进制字符串那部分可能会不同)。
测试 protocol buffer 消息
创建 src/lib.rs 文件,并添加如下内容:
pub mod pb;
创建对应的 pb(文件:src/pb.rs)模块,并添加如下内容:
pub mod getting {
pub mod v1 {
tonic::include_proto!("getting.v1");
}
}
修改 main.rs 文件如下:
use tonic_getting::pb::getting::v1::SigninRequest;
fn main() {
let signin_req = SigninRequest {
email: "yangbajing@gmail.com".to_string(),
password: "Password.2024".to_string(),
};
println!("Signin request is {:?}", signin_req);
}
执行命令 cargo run -q 运行程序,可以正常输出由 protobuf 定义的消息结构体 SigninRequest 的内容。
Signin request is SigninRequest { email: "yangbajing@gmail.com", password: "Password.2024" }
配置 rust-analyzer 分析 build.rs 生成的文件
默认情况下,rust-analyzer 不会分析 build.rs 生成的文件(更准确的说是不会解析在标准源码目录之外的 .rs 文件,如:src、examples、test)。可以通过修改 .vscode/settings.json 文件添加 VSCode 配置来启用对 build.rs 生成的文件的分析。
{
"rust-analyzer.cargo.buildScripts.enable": true,
}
重启 rust-analyzer server 后,就可以通过 F2(或者 Ctrl/CMD + 鼠标点击)跳转到生成的代码定义位置。
定义 gRPC 服务
接下来定义 gRPC 服务并启动它。我们先创建一个 src/grpc.rs 模块,并添加如下代码:
use crate::pb::getting::v1::{
auth_server::Auth, SigninRequest, SigninResponse, SignupRequest, SignupResponse, TokenType,
};
pub struct AuthService;
#[tonic::async_trait]
impl Auth for AuthService {
async fn signin(
&self,
request: tonic::Request<SigninRequest>,
) -> Result<tonic::Response<SigninResponse>, tonic::Status> {
let req = request.into_inner();
println!("Signin request is {:?}", req);
let resp = SigninResponse {
access_token: "".to_string(),
token_type: TokenType::Bearer as i32,
};
Ok(tonic::Response::new(resp))
}
async fn signup(
&self,
request: tonic::Request<SignupRequest>,
) -> Result<tonic::Response<SignupResponse>, tonic::Status> {
let req = request.into_inner();
println!("Signup request is {:?}", req);
let resp = SignupResponse {
code: 0,
..Default::default()
};
Ok(tonic::Response::new(resp))
}
}
添加 tokio 依赖:
cargo add tokio --features full
然后再次修改 main.rs 文件,添加 gRPC 启动服务:
use tonic::transport::Server;
use tonic_getting::{grpc::AuthService, pb::getting::v1::auth_server::AuthServer};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let grpc_addr = "0.0.0.0:9999".parse()?;
println!("The gRPC Server listening to {}", grpc_addr);
Server::builder()
.add_service(AuthServer::new(AuthService)) // 添加 Auth gRPC Service
.serve(grpc_addr).await?; // 绑定到指定网络地址并启动 gRPC 服务
Ok(())
}
我们在命令行终端输入 cargo run -q 启动服务。然后再打开另一个命令行终端,并使用 grpcurl 来测试服务:
grpcurl -plaintext -import-path ./proto \
-proto getting/v1/auth.proto \
-d '{
"email": "yangbajing@gmail.com",
"password": "Password.2024"
}' \
localhost:9999 getting.v1.Auth/Signin
# 下面是输出内容。
{
"accessToken": "L1AhTRgFMiTkQMuGf8PnY6yHAmaV72ESQsEzo0cVWmiodIEx",
"tokenType": "TOKEN_TYPE_BEARER"
}
Protobuf package 与 Rust mod 的映射关系
在示例中,proto 定义在了 getting.v1 package 中,那么生成的 Rust 代码就一定位于 getting::v1 module 中吗?实际上是不会的,tonic 生成的代码并不会包含 getting::v1 这个父模块路径,这些都需要我们自己定义。我们再创建几个 .proto 文件看看 tonic 生成的代码就会明白了。
创建 proto/getting/common/page.proto 文件并添加如下内容:
syntax = "proto3";
package getting.common;
message Pagination {
int64 page = 1;
int64 page_size = 2;
}
创建 proto/getting/v1/user.proto 文件并添加如下内容:
syntax = "proto3";
package getting.v1;
import "getting/common/page.proto";
message UserDto {
int64 id = 1;
string email = 2;
optional string name = 3;
int32 status = 4;
}
message PageUserRequest {
getting.common.Pagination pagination = 1;
}
相应的修改 build.rs 文件,将 "proto/getting/v1/user.proto", "getting/common/page.proto" 添加到 compile 方法的第一个参数数组里(protos)以让 tonic-build 可以编译相关的 .proto 文件。
然后我们在 pb.rs 模块里写个简单的测试:
#[cfg(test)]
mod tests {
use super::getting::v1::*;
#[test]
fn test_user() {
let pagination = Pagination { page: 1, page_size: 20, ..Default::default() };
let page_user_request = PageUserRequest { pagination: Some(pagination) };
println!("Page user request is {:?}", page_user_request);
}
}
并通过 cargo test pb::tests -q 命令运行测试,这时会输出编译错误:
error[E0433]: failed to resolve: could not find `common` in `super`
--> /Users/yangjing/workspaces/grpc-microservices-with-rust/tonic-getting/target/debug/build/tonic-getting-caf65bf194fbeca1/out/getting.v1.rs:409:51
|
409 | pub pagination: ::core::option::Option<super::common::Pagination>,
| ^^^^^^ could not find `common` in `super`
error[E0422]: cannot find struct, variant or union type `Pagination` in this scope
--> src/pb.rs:18:22
|
18 | let pagination = Pagination { page: 1, page_size: 20, ..Default::default() };
| ^^^^^^^^^^ not found in this scope
可以看到,在生成的 getting.v1.rs 文件中,pagination 字段对 Pagination 类型的应用使用了 super::common::Pagination 路径,而 super 在模块树中指的是上级目录,所以这里会报错。那我们在 v1 同级模块添加 pub mod common 模块代码,但这时 tonic::include_proto! 我们应该引入哪个文件呢?我们定位到 getting.v1.rs 所在目录会发现目录内文件如下:
.
├── getting.common.rs
├── getting.v1.rs
看到有 getting.common.rs 文件,我们尝试在 pb.rs 文件中引入 getting::common::* 模块,并修改测试代码:
pub mod getting {
pub mod common {
tonic::include_proto!("getting.common");
}
pub mod v1 {
tonic::include_proto!("getting.v1");
}
}
#[cfg(test)]
mod tests {
use super::getting::common::*;
use super::getting::v1::*;
// ... 测试代码
}
重新运行测试,可以看到测试通过了。
tonic-build 对于 protobuf 的 package 路径,会生成以 .(英文点号)分隔的相应 rust 文件。如 package getting.common 会生成 getting.common.rs 文件,package getting.v1 会生成 getting.v1.rs 文件。而 Rust 代码里面对其它模块类型的引用,则使用 super::common::Pagination 这种相对路径的形式。所以我们在 pb.rs 文件里需要按 proto package 的路径来组织 Rust mod 层次。
你可以试着在
proto目录下创建一个basic.proto文件,并指定package getting。这时,当在user.proto文件中引用basic.proto文件中的类型时,生成的 Rust 文件会是getting.rs,这时在pb.rs文件中引用getting.rs文件中的类型时,就需要使用super::super::BasicType这种形式了。
小结
创建 Rust gRPC 服务快速步骤
这一个简单的 gRPC 服务就完成了,让我们总结下使用 Rust 开发 gRPC 服务的步骤
- 使用
cargo new命令创建项目 - 添加必要的依赖,
tonic、prost、tonic-build、tokio - 引入
.proto文件,通常放到 proto 目录中 - 创建
build.rs文件,并添加tonic-build定义 - 创建
pb模块,并通过tonic::include_proto!宏导入生成的 protobuf 存根 Rust 代码 - 创建 gRPC 服务(使用 tonic)
Protobuf package 路径与 Rust mod 路径的映射关系
package会生成以.(英文点号)分隔的相应 rust 文件- 如:
package getting.common会生成getting.common.rs文件,package getting.v1会生成getting.v1.rs文件。
- 如:
- 在
pb.rs文件中,对package中定义的类型的引用,使用super::common::Pagination这种相对路径的形式。
定义服务
- 定义
XxxService结构体,并实现Xxxtrait - 使用
Server::builder().add_service(XxxServer::new(XxxService)).serve(addr)启动服务