使用 Rust 構建 gRPC 微服務

前言

當前越來越多的公司基於 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 服務端代碼

分別將以下內容拷貝到各個文件:

syntax = "proto3";

package user;

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

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}
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(())
}
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(())
}
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 的同學都比較容易就可以理解。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/daSNckS3Xfmx1azBjAR-vg