文章目录
- [使用 Tonic 构建高性能异步 gRPC 服务](#使用 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 绝对值得你深入学习和实践。