前言

当前越来越多的公司基于 Google gRPC 通信框架来构建微服务体系,比较流行的是使用 Go/Java/C++ 这样的主流编程语言来编写服务端,我们今天来尝试使用 Rust 语言来实现一个 gRPC 服务端/客户端。

打开官方文档可以看到目前 Rust 并不在 gRPC 官方支持的语言列表中:

Supported languages

  • C#
  • C++
  • Dart
  • Go
  • Java
  • Kotlin
  • Node
  • Objective-C
  • PHP
  • Python
  • Ruby

不过不用担心这个问题。我们知道只要某个语言兼容了基于 C/C++ 编写的 gRPC 的核心库,那么该语言就可以完美支持 gRPC。目前 Rust 可以实现 gRPC 的主流 crate 如下:

以上三种任选其一都可以,只是 grpc-rs/grpc-rust 当前还处于开发状态,我们在这里使用 tonic 包。

构建程序

首先检查你的 Rust 版本:

$ rustc --version
rustc 1.61.0 (fe5b13d68 2022-05-18)

tonic 适用于 1.56 及以上,如果低于这个版本,你应该先更新你的 Rust 编译器:

$ rustup update stable

确保你已经提前安装了 protobuf:

$ protoc --version
libprotoc 3.19.4

# macOS 可以通过以下命令安装
$ brew install protobuf

使用 cargo 新建一个项目

$ cargo new grpcrs
$ cd grpcrs
$ cargo run

   Compiling grpcrs v0.1.0 (/Users/lvlv/Documents/project/demo/grpcrs)
    Finished dev [unoptimized + debuginfo] target(s) in 0.55s
     Running `target/debug/grpcrs`
Hello, world!

编辑 cargo.toml 文件:

[package]
name = "grpcrs"
version = "0.1.0"
edition = "2021"

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

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

[dependencies]
tonic = "0.7.2"
tokio = { version = "1.18.2", features = ["macros", "rt-multi-thread"] }
prost = "0.10"

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

创建下列文件:

$ mkdir -p proto/user src/user
$ touch build.rs proto/user/user.proto src/user/{server.rs,client.rs}

当前目录结构:

$ tree -L 3
.
├── Cargo.lock
├── Cargo.toml
├── build.rs # Cargo 构建脚本
├── proto
│   └── user
│       └── user.proto # proto 文件
└── src
    └── user
        ├── client.rs # gRPC 客户端代码
        └── server.rs # gRPC 服务端代码

分别将以下内容拷贝到各个文件:

  • proto/user/user.proto
syntax = "proto3";

package user;

service User {
  rpc Hello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}
  • src/user/server.rs
use tonic::{transport::Server, Request, Response, Status};

use user::user_server::{User, UserServer};
use user::{HelloReply, HelloRequest};

pub mod user {
    tonic::include_proto!("user");
}

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

#[tonic::async_trait]
impl User for UserService {
    async fn hello(&self, request: Request<HelloRequest>) -> Result<Response<HelloReply>, Status> {
        println!("New user request from {:?}", request.remote_addr());

        let reply = user::HelloReply {
            message: format!("Hello {}!", request.into_inner().name),
        };
        Ok(Response::new(reply))
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "127.0.0.1:50051".parse().unwrap();
    let user_service = UserService::default();

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

    Server::builder()
        .add_service(UserServer::new(user_service))
        .serve(addr)
        .await?;

    Ok(())
}
  • src/user/client.rs
use user::user_client::UserClient;
use user::HelloRequest;

pub mod user {
    tonic::include_proto!("user");
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = UserClient::connect("http://127.0.0.1:50051").await?;

    let request = tonic::Request::new(HelloRequest {
        name: "Rick".into(),
    });

    let response = client.hello(request).await?;

    println!("RESPONSE={:?}", response);

    Ok(())
}
  • build.rs
use std::{env, path::PathBuf};

fn main() {
    let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());

    let user_proto = "proto/user/user.proto";

    tonic_build::configure()
        .build_server(true)
        .build_client(true)
        .out_dir(&out_dir)
        .file_descriptor_set_path(&out_dir.join("user_descriptor.bin"))
        .compile(&[user_proto], &["proto"])
        .unwrap_or_else(|err| panic!("protobuf compile failed: {}", err));
}

尝试编译代码:

$ cargo build                
   Compiling proc-macro2 v1.0.39
   Compiling unicode-ident v1.0.0
   Compiling syn v1.0.95
   Compiling libc v0.2.126
   Compiling cfg-if v1.0.0
   Compiling log v0.4.17
   # ... 省略
   Compiling hyper v0.14.19
   Compiling axum v0.5.6
   Compiling hyper-timeout v0.4.1
   Compiling tonic v0.7.2
    Finished dev [unoptimized + debuginfo] target(s) in 21.01s

如果编译通过,现在我们可以尝试执行编译好的程序了。

首先启动 gRPC 服务端程序:

$ cargo run --bin user-server
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/user-server`
UserService listening on 127.0.0.1:50051

重新打开一个 terminal 窗口并执行客户端程序:

$ cargo run --bin user-client
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/user-client`
RESPONSE=Response { metadata: MetadataMap { headers: {"content-type": "application/grpc", "date": "Sun, 29 May 2022 21:54:18 GMT", "grpc-status": "0"} }, message: HelloReply { message: "Hello Rick!" }, extensions: Extensions } # <- 客户端请求成功并返回响应

此时我们切回服务端 terminal 窗口查看日志:

# ...
UserService listening on 127.0.0.1:50051
New user request from Some(127.0.0.1:52147) # <- 客户端调用成功

至此,一个简单的基于 Rust 的 gRPC 服务端/客户端就实现了。上述代码很简陋,相信只要是接触过 gRPC 的同学都比较容易就可以理解。