十八、Rust gRPC 多 proto 演示

本文介绍了如何在Rust项目中使用Tonic库处理多层级的protobuf文件,包括编译、服务定义、服务端和客户端实现的详细步骤,以及如何在GitHubActions中集成protoc编译器。
摘要由CSDN通过智能技术生成

十八、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。

  据说 PingCAP 的 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
  • 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="grpc_server"
path="src/bin/server.rs"

[[bin]]
name="grpc_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> 实现了CloneSyncSend,因此可以跨线程使用。
  • 为服务端生成的 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 的 “包名”(proto文件中的 “package xxx;”),具体来说就是:

    • 注释掉 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 grpc_server
cargo run --bin grpc_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

  • 12
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Rust是一种现代的系统级编程语言,它提供了内存安全、并发性和高性能的特性。在Rust中,多线程同步是通过标准库中的原子类型和同步原语来实现的。 Rust的原子类型(Atomic Types)是一种特殊的数据类型,可以在多个线程之间进行原子操作,保证操作的原子性。常见的原子类型包括AtomicBool、AtomicIsize、AtomicUsize等。通过原子类型,可以实现多线程之间的共享数据的安全访问。 除了原子类型,Rust还提供了一些同步原语,用于实现多线程之间的同步和互斥。其中最常用的是Mutex和RwLock。Mutex提供了互斥锁机制,确保在同一时间只有一个线程可以访问被锁定的数据。RwLock则提供了读写锁机制,允许多个线程同时读取数据,但只允许一个线程进行写操作。 使用Rust进行多线程同步的一般步骤如下: 1. 导入所需的库:在Rust中,需要使用std::sync模块中的相关类型和函数来实现多线程同步。 2. 创建共享数据:定义需要在多个线程之间共享的数据结构。 3. 使用原子类型:对需要在多个线程之间进行原子操作的数据使用原子类型进行封装。 4. 使用同步原语:使用Mutex或RwLock对需要进行互斥访问的数据进行封装。 5. 创建线程:使用std::thread模块创建多个线程,并传递共享数据的引用给每个线程。 6. 进行同步操作:在每个线程中,使用Mutex的lock方法获取锁,并对共享数据进行操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值