Rust 开发 gRPC 服务端和客户端

156334
2022/08/26 21:54:28

Rust 开发 gRPC 主要依赖两个 crate:

tonic 依赖 prost(和axum),它们都提供了 build 工具,用于将 proto 生成 rust 代码。同样,tonic-build 也依赖于 prost-build

项目依赖

# Cargo.toml
[dependencies]
tokio = {version = "1", features = ["full"]}
prost = "0.11"
tonic = "0.8"

[build-dependencies]
tonic-build = "0.8"

构建文件

// build.rs
fn main() {
    tonic_build::configure()
        .out_dir("src/pb") // 生成代码的存放目录
        .compile(
            &["../proto/helloworld.proto"], // 欲生成的 proto 文件列表
            &["../proto"],                  // proto 依赖所在的根目录
        )
        .unwrap();
}

生成代码

mkdir src/pb

因为有 build.rs ,所以运行 cargo run ,将自动生成 proto 对应的 rust 文件。

cargo run

现在,src/pb下应该有一个helloworld.rs,这就是 tonic-buildproto/helloworld.proto 生成的对应的的 rust 代码。由于 rust 模块管理的要求,请创建 src/pb/mod.rs,并输入以下内容:

pub mod helloworld;

多入口

我们采用最简单的多入口模式,将服务端和客户端以单独的文件放在 src目录下。

进入 src目录,并删除main.rs

cd src && rm main.rs

创建 server.rsclient.rs

touch server.rs && touch client.rs

Cargo.toml中配置多入口:

[[bin]]
name="server"
path="src/server.rs"

[[bin]]
name="client"
path="src/client.rs"

服务端

首先,我们定义一个自己的 MyGreeter 服务,并实现 tonic-build 根据 helloworld.proto 生成的代码中的 Greeter

#[derive(Default)]
pub struct MyGreeter {}

#[tonic::async_trait]
impl Greeter for MyGreeter {
    async fn say_hello(
        &self,
        request: tonic::Request<pb::helloworld::HelloRequest>,
    ) -> Result<tonic::Response<pb::helloworld::HelloReply>, tonic::Status> {
        println!("从 {:?} 获取到一个请求", request.remote_addr());
        let reply = pb::helloworld::HelloReply {
            message: format!("[Rust] 你好 {}!", request.into_inner().name),
        };
        Ok(tonic::Response::new(reply))
    }
}

接着,在 main() 函数中启用这个服务:

#[tokio::main]
async fn main() {
    let addr = "127.0.0.1:19527";
    let greeter = MyGreeter::default();

    println!("GreeterServer listening on {}", addr);

    tonic::transport::Server::builder()
        .add_service(GreeterServer::new(greeter))
        .serve(addr.parse().unwrap())
        .await
        .unwrap();
}

完整代码如下:

// src/server.rs

use pb::helloworld::greeter_server::{Greeter, GreeterServer};

mod pb;

#[derive(Default)]
pub struct MyGreeter {}

#[tonic::async_trait]
impl Greeter for MyGreeter {
    async fn say_hello(
        &self,
        request: tonic::Request<pb::helloworld::HelloRequest>,
    ) -> Result<tonic::Response<pb::helloworld::HelloReply>, tonic::Status> {
        println!("从 {:?} 获取到一个请求", request.remote_addr());
        let reply = pb::helloworld::HelloReply {
            message: format!("[Rust] 你好 {}!", request.into_inner().name),
        };
        Ok(tonic::Response::new(reply))
    }
}

#[tokio::main]
async fn main() {
    let addr = "127.0.0.1:19527";
    let greeter = MyGreeter::default();

    println!("GreeterServer listening on {}", addr);

    tonic::transport::Server::builder()
        .add_service(GreeterServer::new(greeter))
        .serve(addr.parse().unwrap())
        .await
        .unwrap();
}

启动服务端:

cargo run --bin server

客户端

// src/client.rs

use pb::helloworld::{greeter_client::GreeterClient, HelloRequest};
use std::env;
mod pb;
#[tokio::main]
async fn main() {
    let grpc_server_addr = env::var("GRPC_SERVER").unwrap_or("127.0.0.1:19527".to_string());
    let grpc_server_addr = format!("http://{}", grpc_server_addr);

    let mut client = GreeterClient::connect(grpc_server_addr).await.unwrap();
    let request = tonic::Request::new(HelloRequest {
        name: "李四".into(),
    });
    let response = client.say_hello(request).await.unwrap();
    println!("{:?}", response);
}

为了让客户端能连接 Rust 编写的服务端和 Go 编写的服务端,我们通过环境变量 GRPC_SERVER 来指定要连接服务端地址。

启动客户端:

cargo run --bin client

指定要连接的服务端地址:

GRPC_SERVER=127.0.0.1:19527 cargo run --bin client