使用 Tonic 构建高性能异步 gRPC 服务

文章目录

使用 Tonic 构建高性能异步 gRPC 服务

在分布式系统开发中,gRPC 作为 Google 开源的高性能 RPC 框架,凭借 Protobuf 二进制序列化的高效性和 HTTP/2 传输的优势,成为服务间通信的首选方案。而在 Rust 生态中,Tonic 框架以其原生异步支持、类型安全、高性能的特性,成为实现 gRPC 服务的最优选择之一。本文将从 Tonic 基础入手,带你从零搭建 gRPC 服务与客户端。

搭建 Tonic 开发环境

安装 Protobuf 编译器

Tonic 依赖 Protobuf 编译器来解析 .proto 文件并生成 Rust 代码,不同系统的安装方式如下:

shell 复制代码
# Linux(Ubuntu/Debian)
sudo apt-get install protobuf-compiler
# macOS
brew install protobuf
# Windows
winget install -e --id Google.Protobuf

安装完成后,执行 protoc --version 验证是否成功。

创建项目并配置依赖

接下来创建一个包含服务端和客户端的 Rust 项目,模拟真实的服务通信场景:

shell 复制代码
cargo new tonic-demo
cd tonic-demo

修改 Cargo.toml,添加核心依赖和构建依赖:

toml 复制代码
# 二进制目标:服务端和客户端
[[bin]]
name = "server"
path = "src/server.rs"

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

[dependencies]
tokio = { version = "1", features = ["full"] }
# tokio 的流式处理需要用到
tokio-stream = { version = "0.1", features = ["full"] }
tonic = "0.14"
tonic-prost = "0.14"
prost = "0.14"
prost-types = "0.14"
anyhow = "1.0"
chrono = "0.4"

[build-dependencies]
tonic-prost-build = "0.14"
anyhow = "1.0"

创建 build.rs 文件,用于配置 Protobuf 编译规则,告诉 Tonic 在构建时自动生成代码:

rust 复制代码
use anyhow::Result;

fn main() -> Result<()> {
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-changed=proto/chat.proto");

    // 编译 proto 目录下的所有 .proto 文件
    tonic_prost_build::compile_protos("proto/chat.proto")?;

    Ok(())
}

入门案例:实现一个双向流式聊天服务

gRPC 支持四种服务方法:一元 RPC(单次请求-响应)、服务端流式、客户端流式、双向流式。其中双向流式适合实时聊天、即时数据推送等场景,本文将基于 Tonic 实现一个简单的双向流式聊天服务,完整覆盖 Protobuf 定义、服务端实现、客户端测试全流程。

定义 Protobuf 接口

gRPC 是接口定义优先的,首先需要通过 Protobuf 定义服务接口和数据结构。在项目根目录创建 proto 文件夹,新建 chat.proto 文件:

protobuf 复制代码
syntax = "proto3";
package chat; // 包名,用于生成 Rust 模块

// 定义聊天消息结构
message ChatMessage {
    string username = 1; // 用户名
    string content = 2; // 消息内容
    string timestamp = 3; // 发送时间戳
}

// 定义聊天服务接口
service ChatService {
    // 双向流式 RPC:客户端和服务端可同时发送消息
    rpc ChatStream (stream ChatMessage) returns (stream ChatMessage);
}

生成 Rust 代码

执行 cargo build 命令,Tonic 会根据 build.rs 的配置,编译 chat.proto 并生成对应的 Rust 代码,生成的代码位于:

plaintext 复制代码
target/debug/build/tonic-demo-*/out/chat.rs

需要注意的是:在使用时,在代码中可通过 tonic::include_proto!("chat") 引入生成的模块。

实现服务端逻辑

服务端需要实现 Protobuf 定义的 ChatService 特征,核心逻辑是接收客户端消息,并将消息广播给所有连接的客户端。这里利用 Tokio 的广播通道实现消息分发,新建 src/server.rs

rust 复制代码
use tokio::sync::Mutex;
use tokio::sync::broadcast;
use tokio::sync::mpsc;
use tokio_stream::StreamExt;
use tokio_stream::wrappers::BroadcastStream;
use tokio_stream::wrappers::ReceiverStream;
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
use tonic::transport::Server;
use tonic::{Request, Response, Status, Streaming};

use crate::chat::ChatMessage;
use crate::chat::chat_service_server::ChatService;
use crate::chat::chat_service_server::ChatServiceServer;

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

#[derive(Debug, Default)]
pub struct ChatServer {
    // 广播通道发送端,用于向所有客户端广播消息
    // 用 Mutex 包裹,实现异步环境的内部可变性
    broadcaster: Mutex<Option<broadcast::Sender<ChatMessage>>>,
}

#[tonic::async_trait]
impl ChatService for ChatServer {
    // 双向流式方法的实现,返回值为 Stream<Item = Result<ChatMessage, Status>>
    type ChatStreamStream = ReceiverStream<Result<ChatMessage, Status>>;

    async fn chat_stream(
        &self,
        request: Request<Streaming<ChatMessage>>,
    ) -> Result<Response<Self::ChatStreamStream>, Status> {
        // 获取客户端发送的消息流
        let mut stream = request.into_inner();

        // 初始化广播通道(首次连接时创建)
        let mut broadcaster_lock = self.broadcaster.lock().await;
        let tx = match &*broadcaster_lock {
            Some(sender) => sender.clone(),
            None => {
                // 首次连接:创建广播通道,并存储到服务端实例中
                let (sender, _) = broadcast::channel(128);
                *broadcaster_lock = Some(sender.clone());
                sender
            }
        };
        // 释放锁,避免长时间占用
        drop(broadcaster_lock);

        // 当前客户端订阅广播通道
        let rx = tx.subscribe();

        // 处理 broadcast 通道的 Lagged 错误(消费者消息落后)
        let broadcast_stream = BroadcastStream::new(rx).filter_map(|msg| match msg {
            Ok(msg) => Some(Ok(msg)),
            Err(BroadcastStreamRecvError::Lagged(_)) => {
                eprintln!("客户端消息落后,跳过旧消息");
                None
            }
        });

        //  创建客户端下行通道
        let (client_tx, client_rx) = mpsc::channel(128);
        let response_stream = ReceiverStream::new(client_rx);

        // 监听客户端发送的消息,广播给所有人
        tokio::spawn(async move {
            while let Some(result) = stream.next().await {
                match result {
                    Ok(msg) => {
                        // 广播消息,忽略发送错误(无客户端时正常)
                        let _ = tx.send(msg);
                    }
                    Err(e) => {
                        eprintln!("接收客户端消息失败: {}", e);
                        break;
                    }
                }
            }
            eprintln!("客户端断开连接(上行)");
        });

        // 转发广播消息给当前客户端
        tokio::spawn(async move {
            tokio::pin!(broadcast_stream);
            while let Some(msg) = broadcast_stream.next().await {
                if client_tx.send(msg).await.is_err() {
                    // 客户端断开连接,终止任务
                    break;
                }
            }
            eprintln!("客户端断开连接(下行)");
        });

        // 返回流给客户端
        Ok(Response::new(response_stream))
    }
}

// 服务端入口函数
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 绑定服务地址
    let addr = "[::1]:50051".parse()?;
    // 创建服务实例
    let chat_server = ChatServer::default();

    println!("Chat server running on {}", addr);

    // 启动 gRPC 服务
    Server::builder()
        .add_service(ChatServiceServer::new(chat_server))
        .serve(addr)
        .await?;

    Ok(())
}

实现客户端逻辑

客户端需要连接服务端,同时执行两个操作:读取用户输入并发送消息、监听服务端广播的消息并打印。新建 src/client.rs

rust 复制代码
use anyhow::Result;
use tokio::{
    io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader},
    sync::mpsc,
};
use tokio_stream::{StreamExt, wrappers::ReceiverStream};

use crate::chat::{ChatMessage, chat_service_client::ChatServiceClient};

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

#[tokio::main]
async fn main() -> Result<()> {
    // 连接服务端
    let mut client = ChatServiceClient::connect("http://[::1]:50051").await?;
    println!("Connected to chat server");

    // 获取用户名
    println!("Enter your username: ");
    io::stdout().flush().await?;
    let mut reader = BufReader::new(io::stdin());
    let mut username = String::new();
    reader.read_line(&mut username).await?;
    let username = username.trim().to_string();

    // 创建消息发送通道
    let (tx, rx) = mpsc::channel(128);
    let request = tonic::Request::new(ReceiverStream::new(rx));

    // 发起双向流请求,获取服务端响应流
    let mut response_stream = client.chat_stream(request).await?.into_inner();

    // 启动异步任务:监听服务端消息并打印
    tokio::spawn(async move {
        while let Some(Ok(msg)) = response_stream.next().await {
            println!("[{}] {}: {}", msg.timestamp, msg.username, msg.content);
        }
        println!("Disconnected from server");
    });

    // 读取用户输入并发送消息
    let mut lines = reader.lines();

    while let Some(line) = lines.next_line().await? {
        if line.eq_ignore_ascii_case("exit") {
            println!("Exiting chat...");
            break;
        }

        // 构造聊天消息
        let msg = ChatMessage {
            username: username.clone(),
            content: line,
            timestamp: chrono::Local::now().format("%H:%M:%S").to_string(),
        };

        // 发送消息到服务端
        if tx.send(msg).await.is_err() {
            println!("Failed to send message");
            break;
        }
    }

    Ok(())
}

测试服务

分别启动服务端和客户端,测试双向流式通信:

shell 复制代码
# 启动服务端
cargo run --bin server

# 新终端启动多个客户端
cargo run --bin client

输入用户名后,即可发送消息,所有连接的客户端都会实时收到广播消息,实现简单的群聊功能。

Tonic 进阶

拦截器(中间件)

拦截器类似于 Web 框架的中间件,可在请求/响应处理前后执行自定义逻辑,如认证、日志记录、指标收集等。下面实现一个简单的 Token 认证拦截器:

rust 复制代码
use tonic::service::InterceptorLayer;
use tonic::{Request, Status};

// 自定义认证拦截器
#[derive(Debug, Clone, Copy)]
pub struct AuthInterceptor;

impl Interceptor for AuthInterceptor {
    fn call(&mut self, request: Request<()>) -> Result<Request<()>, Status> {
        // 从请求头中获取 Token
        let token = request
            .metadata()
            .get("authorization")
            .and_then(|v| v.to_str().ok())
            .unwrap_or("");

        // 简单验证 Token,实际场景需结合密钥验证
        if token != "Bearer tonic-demo-token" {
            return Err(Status::unauthenticated("Invalid or missing token"));
        }

        Ok(request)
    }
}

use tonic::service::InterceptorLayer;

// 服务端启动时添加拦截器
Server::builder()
    .layer(InterceptorLayer::new(AuthInterceptor))
    .add_service(ChatServiceServer::new(chat_server))
    .serve(addr)
    .await?;

客户端发送请求时,需在请求头中添加 Token:

rust 复制代码
let mut request = tonic::Request::new(ReceiverStream::new(rx));
request
    .metadata_mut()
    .insert("authorization", "Bearer tonic-demo-token".parse()?);

TLS 加密传输

生产环境中,服务间通信需要加密,Tonic 基于 rustls 原生支持 TLS 加密。只需修改服务端和客户端的连接配置,即可实现加密通信,具体可参考 Tonic 官方示例

四种 gRPC 服务方法对比

Tonic 完全支持 gRPC 的四种服务方法,适用于不同场景:

方法类型 描述 适用场景
一元 RPC 客户端发送单次请求,服务端返回单次响应 简单查询、接口调用(如用户登录)
服务端流式 RPC 客户端发送单次请求,服务端返回流式响应 大数据量返回(如日志查询、文件下载)
客户端流式 RPC 客户端发送流式请求,服务端返回单次响应 大数据量上传(如文件上传、批量数据提交)
双向流式 RPC 客户端和服务端同时发送流式消息,双向独立通信 实时通信(如聊天、实时监控、行情推送)

总结

Tonic 作为 Rust 生态中成熟的 gRPC 框架,凭借其异步原生、类型安全、高性能的优势,极大地降低了 Rust 开发 gRPC 服务的门槛。如果你正在用 Rust 构建分布式服务,Tonic 绝对值得你深入学习和实践。

相关推荐
captain3762 小时前
JDBC(Java Data Base Connectivity)
java·开发语言
南境十里·墨染春水2 小时前
C++笔记 STL——vector
开发语言·c++·笔记
思麟呀2 小时前
Epoll的学习,在select和poll的基础上
网络·数据库·sql·学习·tcp/ip
故事和你912 小时前
洛谷-算法2-2-常见优化技巧3
开发语言·数据结构·c++·算法·深度优先·动态规划·图论
foundbug9992 小时前
MATLAB时频分析工具箱:基于FRFT的信号检测与参数估计
开发语言·matlab
DevilSeagull2 小时前
Rust 方法语法:从定义到实践
开发语言·后端·rust
charlie1145141912 小时前
通用GUI编程技术——图形渲染实战(三十七)——D3D11初始化与SwapChain:从零搭建GPU渲染框架
开发语言·c++·3d·图形渲染
陈天伟教授2 小时前
GPT Image 2-城市海报
开发语言·人工智能·gpt·神经网络
原来是猿2 小时前
线程安全的单例模式
linux·服务器·开发语言·单例模式·策略模式