Rust gRPC构建工具grpc-build-core的使用:高效生成gRPC客户端和服务端代码的Rust插件库

Rust gRPC构建工具grpc-build-core的使用:高效生成gRPC客户端和服务端代码的Rust插件库

grpc-build 提供了一种灵活的方式来管理 protobuf 文件并生成 tonic 所需的 gRPC 代码。

它构建在 tonic_build 之上,并通过编译目录中的所有 protobuf 文件来扩展其功能。此外,该库还添加了另一个功能:完整 proto 名称注解。这在您想要使用完整名称(包 + 消息名称)来标识 protobuf 消息的情况下可能很有用。因此,对于每个顶级 protobuf 消息,该库会为其生成的结构体添加一个方法,返回其完整的 proto 名称。

给定以下 protobuf 定义:

// my_message.proto
package grpc_build;
message Message {}

该库将生成标准的 Rust 代码以及每个消息的额外实现。

// Message.rs (generated)
struct Message {}

impl NamedMessage for Message {
    /// This returns package (grpc-build) + message name (Message).
    const NAME: &'static str = "grpc_build.MyMessage"
}

如果 protobuf 内容有效(值得进行 lint 检查),grpc-build 将处理 protobuf 导入,并生成 mod.rs 文件以允许编译器找到生成的代码。该文件将放置在输出目录中。

它既可以作为库直接在项目中使用,也可以作为二进制文件在 CI 管道中使用。

入门指南

作为二进制文件使用

获取最新的二进制版本并在您的 CI 管道中使用它。

grpc-build build --in-dir="<protobuf directory>" --out-dir="<codegen>"

根据需求,您可以使用 --build-client (-c) 和 --build-server (-s) 标志生成 gRPC 客户端和/或服务器。

要覆盖输出目录的内容,请使用 --force (-f) 标志。

// both client and server, overwriting the existing protogen
grpc-build build -c -s --in-dir="<protobuf directory>" --out-dir="<codegen>" -f

作为库使用

使用 grpc_build 作为库最方便的方式是利用 Rust 的 build.rs 文件。不要忘记将 grpc_build 添加到构建依赖项列表中。

// build.rs
use grpc_build::Builder;

fn main() {
    Builder::new()
        .build_client(true)
        .build_server(true)
        .force(true)
        .out_dir("src/protogen")
        .build("protos")
        .unwrap();
}

如果您想设置高级编译选项(如为生成的类型添加额外的 #[derive]),请使用 build_with_config 函数,该函数暴露了底层的 tonic_build::Builder

更高级的用法是自行使用 get_protosrefactor 函数。以下示例与上面的示例几乎相同,只是您不会自动获得 NamedMessage 特性。

fn main() {
    let proto_src_dir = "protos";
    let proto_out_dir = "src/protogen";

    let protos: Vec<_> = crate::base::get_protos(proto_src_dir).collect();

    grpc_build::prepare_out_dir(proto_out_dir).unwrap();

    tonic_build::configure()
        .out_dir(proto_out_dir)
        .build_server(true)
        .build_client(true)
        .compile(&protos, &["."])
        .unwrap();

    grpc_build::refactor(proto_out_dir).unwrap();
}

许可证

该项目根据 MIT 许可证进行许可。

完整示例代码

以下是使用 grpc-build-core 的完整示例:

// Cargo.toml 依赖配置
// [build-dependencies]
// grpc-build-core = "0.5.0"

// build.rs 文件示例
use grpc_build::Builder;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 配置构建器生成客户端和服务器代码
    Builder::new()
        .build_client(true)    // 生成客户端代码
        .build_server(true)    // 生成服务器代码
        .force(true)           // 强制覆盖现有文件
        .out_dir("src/protogen") // 输出目录
        .build("protos")       // protobuf 文件目录
        .unwrap();
    
    Ok(())
}

// 高级配置示例
fn advanced_build() -> Result<(), Box<dyn std::error::Error>> {
    let proto_src_dir = "protos";
    let proto_out_dir = "src/protogen";

    // 获取所有 protobuf 文件
    let protos: Vec<_> = grpc_build::get_protos(proto_src_dir).collect();
    
    // 准备输出目录
    grpc_build::prepare_out_dir(proto_out_dir)?;

    // 使用 tonic_build 配置进行编译
    tonic_build::configure()
        .out_dir(proto_out_dir)
        .build_server(true)
        .build_client(true)
        .compile(&protos, &["."])?;

    // 重构生成的代码
    grpc_build::refactor(proto_out_dir)?;
    
    Ok(())
}
// protos/example.proto
syntax = "proto3";

package example;

service Greeter {
    rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
    string name = 1;
}

message HelloResponse {
    string message = 1;
}

使用此配置后,grpc-build-core 会自动生成相应的 Rust 代码,包括带有完整 proto 名称注解的消息结构体。

完整示例demo

基于上述内容,以下是一个完整的项目示例:

Cargo.toml 配置:

[package]
name = "grpc-example"
version = "0.1.0"
edition = "2021"

[dependencies]
tonic = "0.8"
prost = "0.11"

[build-dependencies]
grpc-build-core = "0.5.0"
tonic-build = "0.8"

build.rs 文件:

use grpc_build::Builder;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 基础配置:生成客户端和服务器代码
    Builder::new()
        .build_client(true)    // 启用客户端代码生成
        .build_server(true)    // 启用服务器代码生成
        .force(true)           // 强制覆盖现有文件
        .out_dir("src/protogen") // 指定输出目录
        .build("protos")       // protobuf 文件所在目录
        .unwrap();
    
    Ok(())
}

protobuf 文件 (protos/example.proto):

syntax = "proto3";

package example;

// 定义 Greeter 服务
service Greeter {
    rpc SayHello (HelloRequest) returns (HelloResponse);
    rpc SayGoodbye (GoodbyeRequest) returns (GoodbyeResponse);
}

// 请求消息
message HelloRequest {
    string name = 1;
    int32 age = 2;
}

// 响应消息
message HelloResponse {
    string message = 1;
    bool success = 2;
}

// 另一个请求消息
message GoodbyeRequest {
    string name = 1;
    string reason = 2;
}

// 另一个响应消息
message GoodbyeResponse {
    string farewell = 1;
    int32 status_code = 2;
}

src/main.rs 使用示例:

mod protogen;
use protogen::example::{greeter_client::GreeterClient, HelloRequest};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 创建 gRPC 客户端
    let mut client = GreeterClient::connect("http://[::1]:50051").await?;
    
    // 创建请求消息
    let request = tonic::Request::new(HelloRequest {
        name: "World".to_string(),
        age: 25,
    });
    
    // 发送请求
    let response = client.say_hello(request).await?;
    
    println!("RESPONSE={:?}", response);
    
    Ok(())
}

src/lib.rs 服务器示例:

mod protogen;
use tonic::{Request, Response, Status};

use protogen::example::{
    greeter_server::{Greeter, GreeterServer},
    HelloRequest, HelloResponse, GoodbyeRequest, GoodbyeResponse
};

// 实现 Greeter 服务
#[derive(Debug, Default)]
pub struct MyGreeter {}

#[tonic::async_trait]
impl Greeter for MyGreeter {
    async fn say_hello(
        &self,
        request: Request<HelloRequest>,
    ) -> Result<Response<HelloResponse>, Status> {
        let req = request.into_inner();
        let reply = HelloResponse {
            message: format!("Hello {}! Age: {}", req.name, req.age),
            success: true,
        };
        Ok(Response::new(reply))
    }

    async fn say_goodbye(
        &self,
        request: Request<GoodbyeRequest>,
    ) -> Result<Response<GoodbyeResponse>, Status> {
        let req = request.into_inner();
        let reply = GoodbyeResponse {
            farewell: format!("Goodbye {}! Reason: {}", req.name, req.reason),
            status_code: 200,
        };
        Ok(Response::new(reply))
    }
}

// 启动服务器函数
pub async fn start_server() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "[::1]:50051".parse()?;
    let greeter = MyGreeter::default();
    
    tonic::transport::Server::builder()
        .add_service(GreeterServer::new(greeter))
        .serve(addr)
        .await?;
    
    Ok(())
}

这个完整示例展示了如何使用 grpc-build-core 从 protobuf 定义生成 Rust 代码,并创建完整的 gRPC 客户端和服务器实现。生成的代码会自动包含 NamedMessage 特性,为每个消息提供完整的 proto 名称标识。


1 回复

grpc-build-core:Rust gRPC构建工具使用指南

介绍

grpc-build-core是一个专为Rust设计的gRPC构建工具,能够高效生成gRPC客户端和服务端代码。该库作为prost-build的扩展,提供了更简洁的配置方式和更强大的代码生成能力,特别适合需要高性能gRPC通信的Rust项目。

主要特性

  • 基于prost和tonic构建
  • 支持proto3语法
  • 自动生成异步客户端和服务端代码
  • 可配置的代码生成选项
  • 与Cargo构建系统无缝集成

安装方法

在Cargo.toml中添加依赖:

[dependencies]
grpc-build-core = "0.1"
prost = "0.11"
tonic = "0.8"

[build-dependencies]
grpc-build-core = "0.1"

使用方法

1. 创建build.rs构建脚本

// build.rs
use grpc_build_core::Builder;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    Builder::new()
        .build_server(true)
        .build_client(true)
        .out_dir("src/generated")
        .compile(&["proto/hello.proto"], &["proto/"])?;
    Ok(())
}

2. 定义Proto文件

// proto/hello.proto
syntax = "proto3";

package hello;

service Greeter {
    rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
    string name = 1;
}

message HelloResponse {
    string message = 1;
}

3. 使用生成的代码

服务端实现示例:

// src/main.rs
mod generated;
use generated::hello::{greeter_server::{Greeter, GreeterServer}, HelloRequest, HelloResponse};
use tonic::{transport::Server, Request, Response, Status};

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

#[tonic::async_trait]
impl Greeter for MyGreeter {
    async fn say_hello(
        &self,
        request: Request<HelloRequest>,
    ) -> Result<Response<HelloResponse>, Status> {
        let reply = HelloResponse {
            message: format!("Hello {}!", request.into_inner().name),
        };
        Ok(Response::new(reply))
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "[::1]:50051".parse()?;
    let greeter = MyGreeter::default();

    Server::builder()
        .add_service(GreeterServer::new(greeter))
        .serve(addr)
        .await?;

    Ok(())
}

客户端使用示例:

// src/client.rs
mod generated;
use generated::hello::{greeter_client::GreeterClient, HelloRequest};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = GreeterClient::connect("http://[::1]:50051").await?;
    
    let request = tonic::Request::new(HelloRequest {
        name: "World".into(),
    });
    
    let response = client.say_hello(request).await?;
    println!("RESPONSE={:?}", response);
    
    Ok(())
}

配置选项

Builder::new()
    .build_server(true)        // 生成服务端代码
    .build_client(true)        // 生成客户端代码
    .out_dir("src/generated")  // 输出目录
    .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") // 添加属性
    .compile_protos(&["proto/hello.proto"], &["proto/"])?;

注意事项

  1. 确保protobuf编译器(protoc)已安装并在PATH中
  2. 生成的代码会在每次构建时更新
  3. 建议将生成的文件加入.gitignore
  4. 支持自定义代码生成选项和扩展

这个工具极大简化了Rust项目中gRPC代码的生成和使用过程,让开发者能够更专注于业务逻辑的实现。

完整示例demo

以下是一个完整的gRPC服务示例,包含服务端和客户端的完整实现:

项目结构:

my-grpc-project/
├── Cargo.toml
├── build.rs
├── proto/
│   └── hello.proto
└── src/
    ├── main.rs
    └── client.rs

Cargo.toml:

[package]
name = "my-grpc-project"
version = "0.1.0"
edition = "2021"

[dependencies]
grpc-build-core = "0.1"
prost = "0.11"
tonic = "0.8"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }

[build-dependencies]
grpc-build-core = "0.1"

build.rs:

use grpc_build_core::Builder;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    Builder::new()
        .build_server(true)
        .build_client(true)
        .out_dir("src/generated")
        .compile(&["proto/hello.proto"], &["proto/"])?;
    Ok(())
}

proto/hello.proto:

syntax = "proto3";

package hello;

service Greeter {
    rpc SayHello (HelloRequest) returns (HelloResponse);
    rpc SayHelloStream (HelloRequest) returns (stream HelloResponse);
}

message HelloRequest {
    string name = 1;
    int32 count = 2;
}

message HelloResponse {
    string message = 1;
    int32 sequence = 2;
}

src/main.rs (服务端):

// 引入生成的gRPC代码
mod generated;
use generated::hello::{
    greeter_server::{Greeter, GreeterServer},
    HelloRequest, HelloResponse
};
use tonic::{transport::Server, Request, Response, Status};
use tokio_stream::wrappers::ReceiverStream;
use tokio::sync::mpsc;

// 实现Greeter服务
#[derive(Default)]
pub struct MyGreeter {}

#[tonic::async_trait]
impl Greeter for MyGreeter {
    // 简单的SayHello方法
    async fn say_hello(
        &self,
        request: Request<HelloRequest>,
    ) -> Result<Response<HelloResponse>, Status> {
        let req = request.into_inner();
        let reply = HelloResponse {
            message: format!("Hello {}!", req.name),
            sequence: 0,
        };
        Ok(Response::new(reply))
    }

    // 流式响应的SayHelloStream方法
    async fn say_hello_stream(
        &self,
        request: Request<HelloRequest>,
    ) -> Result<Response<Self::SayHelloStreamStream>, Status> {
        let req = request.into_inner();
        let (tx, rx) = mpsc::channel(4);
        
        // 在后台任务中生成流式响应
        tokio::spawn(async move {
            for i in 0..req.count {
                let response = HelloResponse {
                    message: format!("Hello {} - message {}", req.name, i + 1),
                    sequence: i as i32 + 1,
                };
                
                if tx.send(Ok(response)).await.is_err() {
                    break;
                }
                
                tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
            }
        });

        Ok(Response::new(ReceiverStream::new(rx)))
    }
}

type SayHelloStreamStream = 
    tonic::codec::Streaming<Result<HelloResponse, Status>>;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "[::1]:50051".parse()?;
    let greeter = MyGreeter::default();

    println!("gRPC server starting on {}", addr);

    Server::builder()
        .add_service(GreeterServer::new(greeter))
        .serve(addr)
        .await?;

    Ok(())
}

src/client.rs (客户端):

// 引入生成的gRPC代码
mod generated;
use generated::hello::{greeter_client::GreeterClient, HelloRequest};
use tokio_stream::StreamExt;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 连接到gRPC服务器
    let mut client = GreeterClient::connect("http://[::1]:50051").await?;
    
    // 测试普通RPC调用
    println!("Testing simple RPC call...");
    let request = tonic::Request::new(HelloRequest {
        name: "World".into(),
        count: 0,
    });
    
    let response = client.say_hello(request).await?;
    println!("Simple response: {:?}", response.into_inner());
    
    // 测试流式RPC调用
    println!("\nTesting streaming RPC call...");
    let stream_request = tonic::Request::new(HelloRequest {
        name: "Streaming".into(),
        count: 5,
    });
    
    let mut stream = client.say_hello_stream(stream_request).await?.into_inner();
    
    while let Some(item) = stream.next().await {
        match item {
            Ok(response) => println!("Stream response: {:?}", response),
            Err(e) => eprintln!("Error in stream: {}", e),
        }
    }
    
    Ok(())
}

运行说明:

  1. 首先确保安装了protobuf编译器:
# Ubuntu/Debian
sudo apt-get install protobuf-compiler

# macOS
brew install protobuf
  1. 启动服务端:
cargo run
  1. 在另一个终端运行客户端:
cargo run --bin client

这个完整示例展示了如何使用grpc-build-core创建一个支持普通RPC调用和流式RPC调用的完整gRPC服务,包含了服务端和客户端的完整实现。

回到顶部