跳转到正文
zeno's blog
返回

Rust Web:Tonic gRPC Server 结构指南

专题: Rust Web

Table of contents

Open Table of contents

项目结构

mini_tarkov_server/
├── Cargo.toml
├── build.rs              # protobuf 编译脚本
├── proto/
│   └── game.proto        # protobuf 服务定义
└── src/
    └── main.rs           # server 入口

1. Protobuf 服务定义 (proto/game.proto)

syntax = "proto3";

package game;

service GameService {
  rpc Login (LoginRequest) returns (LoginResponse);
  rpc GetInventory (GetInventoryRequest) returns (GetInventoryResponse);
}

message LoginRequest {
  string username = 1;
  string password = 2;
}

message LoginResponse {
  string token = 1;
  string player_id = 2;
}

message GetInventoryRequest {
  string player_id = 1;
}

message Item {
  string id = 1;
  string name = 2;
  int32 quantity = 3;
}

message GetInventoryResponse {
  repeated Item items = 1;
}

2. Cargo.toml

[package]
name = "mini_tarkov_server"
version = "0.1.0"
edition = "2024"

[dependencies]
tonic = "0.13"
prost = "0.14"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

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

关键依赖说明:

3. 构建脚本 (build.rs)

fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_build::compile_protos("proto/game.proto")?;
    Ok(())
}

cargo build 时,tonic-build 会:

  1. 读取 proto/game.proto
  2. 生成消息结构体(LoginRequest, LoginResponse 等)
  3. 生成 server trait(GameService)和 client stub
  4. 输出到 target/ 下的 OUT_DIR,通过 include_proto! 引入

4. Server 实现 (src/main.rs)

use tonic::{transport::Server, Request, Response, Status};

// 引入 protobuf 生成的代码
pub mod game {
    tonic::include_proto!("game");
}

use game::game_service_server::{GameService, GameServiceServer};
use game::{
    GetInventoryRequest, GetInventoryResponse, Item, LoginRequest, LoginResponse,
};

// 定义 service 实现结构体
#[derive(Debug, Default)]
pub struct GameServiceImpl {}

// 实现生成的 trait
#[tonic::async_trait]
impl GameService for GameServiceImpl {
    async fn login(
        &self,
        request: Request<LoginRequest>,
    ) -> Result<Response<LoginResponse>, Status> {
        let req = request.into_inner();
        println!("Login attempt: {}", req.username);

        // 实际项目中这里做认证逻辑
        let response = LoginResponse {
            token: "fake-jwt-token".to_string(),
            player_id: "player_001".to_string(),
        };

        Ok(Response::new(response))
    }

    async fn get_inventory(
        &self,
        request: Request<GetInventoryRequest>,
    ) -> Result<Response<GetInventoryResponse>, Status> {
        let req = request.into_inner();
        println!("GetInventory for player: {}", req.player_id);

        let response = GetInventoryResponse {
            items: vec![
                Item {
                    id: "item_1".to_string(),
                    name: "AK-47".to_string(),
                    quantity: 1,
                },
                Item {
                    id: "item_2".to_string(),
                    name: "Bandage".to_string(),
                    quantity: 5,
                },
            ],
        };

        Ok(Response::new(response))
    }
}

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

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

    Server::builder()
        .add_service(GameServiceServer::new(service))
        .serve(addr)
        .await?;

    Ok(())
}

5. 核心模式总结

.proto 文件

    ▼  (build.rs / tonic-build)
生成的 Rust 代码 (trait + 消息结构体)

    ▼  (impl trait for YourStruct)
你的业务逻辑

    ▼  (Server::builder().add_service())
运行中的 gRPC server

流程就三步

  1. .proto 定义接口和消息
  2. build.rs 编译 proto → 生成 trait
  3. impl 这个 trait,填入业务逻辑,挂到 Server::builder()

6. Streaming RPC(补充)

tonic 支持四种 RPC 模式:

模式proto 写法Rust 返回类型
Unaryrpc Foo(Req) returns (Res)Result<Response<Res>, Status>
Server streamingrpc Foo(Req) returns (stream Res)Result<Response<Self::FooStream>, Status>
Client streamingrpc Foo(stream Req) returns (Res)Result<Response<Res>, Status>,参数为 Request<Streaming<Req>>
Bidirectionalrpc Foo(stream Req) returns (stream Res)两者结合

Server streaming 示例:

type GetEventsStream = Pin<Box<dyn Stream<Item = Result<Event, Status>> + Send>>;

async fn get_events(
    &self,
    request: Request<EventFilter>,
) -> Result<Response<Self::GetEventsStream>, Status> {
    let (tx, rx) = tokio::sync::mpsc::channel(32);

    tokio::spawn(async move {
        // 持续推送事件
        tx.send(Ok(Event { ... })).await.unwrap();
    });

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

7. 运行和测试

# 构建并运行
cargo run

# 用 grpcurl 测试(需安装 grpcurl)
grpcurl -plaintext -d '{"username":"player1","password":"123"}' \
  localhost:50051 game.GameService/Login

分享这篇文章:

上一篇
Rust Web:服务器错误处理体系
下一篇
Rust Web:一个请求的完整生命周期