1. 简介
在本代码实验室中,您将使用 gRPC-Rust 创建一个客户端和服务器,作为用 Rust 编写的路由映射应用程序的基础。
在本教程结束时,您将拥有一个客户端,该客户端使用 gRPC 协议的 官方 Rust 实现 连接到远程服务器,以获取地图上特定坐标位置的名称或邮政地址。一个功能齐全的应用程序可能会使用这种客户端-服务器设计来枚举或汇总沿途的兴趣点。
该服务定义在一个 Protocol Buffers 文件中,该文件将用于生成客户端和服务器的样板代码,以便它们可以相互通信,从而节省您实现该功能的时间和精力。
生成的代码不仅处理服务器和客户端之间通信的复杂性,还处理数据序列化和反序列化。
学习内容
- 如何使用 Protocol Buffers 定义服务 API。
- 如何使用自动化代码生成技术,从 Protocol Buffer 定义构建基于 gRPC 的客户端。
- 了解使用 gRPC 进行客户端-服务器通信。
本代码实验室面向刚接触 gRPC 或希望复习 gRPC 的 Rust 开发者,以及任何其他对构建分布式系统感兴趣的人。无需具备 gRPC 使用经验。
2. 准备工作
前提条件
请确保您已安装以下软件:
获取代码
为了避免您从零开始,本代码实验室提供了应用程序源代码的框架供您完成。以下步骤将向您展示如何完成应用程序,包括使用协议缓冲区编译器插件生成样板 gRPC 代码。
首先,创建 codelab 工作目录并进入该目录:
mkdir grpc-rust-getting-started && cd grpc-rust-getting-started
下载并解压代码实验室:
curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/2026.tar.gz \
| tar xvz --strip-components=4 \
grpc-codelabs-2026/codelabs/grpc-rust-getting-started/start_here
或者,您可以下载仅包含 codelab 目录的 .zip 文件,然后手动解压缩。
完整的源代码是可在 GitHub 上获取如果您想跳过输入实现代码。
3. 定义服务
第一步是使用 Protocol Buffers 定义应用程序的 gRPC 服务、其 RPC 方法及其请求和响应消息类型。您的服务将提供:
- 服务器实现的 RPC 方法,客户端调用的 RPC 方法为
GetFeature。 - 消息类型
Point和Feature是使用GetFeature方法时客户端和服务器之间交换的数据结构。客户端在其向服务器发出的GetFeature请求中提供地图坐标作为Point,服务器回复相应的Feature,描述位于这些坐标处的事物。
该 RPC 方法及其消息类型都将在提供的源代码的 proto/routeguide.proto 文件中定义。
协议缓冲区通常被称为 protobuf。有关 gRPC 术语的更多信息,请参阅 gRPC 的 核心概念、架构和生命周期。
服务方法
我们先定义服务方法,然后定义消息类型 Point 和 Feature。proto/routeguide.proto 文件有一个名为 RouteGuide 的 service 结构,它定义了应用程序服务提供的一个或多个方法。
在 RouteGuide 定义中添加 rpc 方法 GetFeature。如前所述,此方法将根据给定的坐标集查找位置的名称或地址,因此对于给定的 Point,GetFeature 返回 Feature:
service RouteGuide {
// Definition of the service goes here
// Obtains the feature at a given position.
rpc GetFeature(Point) returns (Feature) {}
}
这是一个一元 RPC 方法:简单的 RPC客户端向服务器发送请求并等待响应返回,就像本地函数调用一样。
消息类型
在源代码的 proto/routeguide.proto 文件中,首先定义 Point 消息类型。Point 表示地图上的纬度-经度坐标对。在本代码实验中,坐标请使用整数:
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
数字 1 和 2 是 message 结构中每个字段的唯一 ID 号。
接下来,定义 Feature 消息类型。Feature 使用 string 字段来表示 Point 指定位置的某物的名称或邮政地址:
message Feature {
// The name or address of the feature.
string name = 1;
// The point where the feature is located.
Point location = 2;
}
4. 生成客户端和服务端代码
我们已经将 generated/ 目录中的 .proto 文件生成的代码(包括您上面添加的所有代码)提供给您。不过,我们想花点时间解释一下代码生成是如何工作的。
我们的 .proto 文件描述了客户端或服务器使用的所有结构和函数。我们使用 Cargo 构建脚本 (build.rs) 和 grpc-protobuf-build crate 来自动生成此代码。
在Cargo.toml中,我们在[build-dependencies]下面添加grpc-protobuf-build。
在 build.rs 中,我们配置 grpc_protobuf_build::CodeGen 将 proto/routeguide.proto 编译到 generated/ 目录中。关键语句如下:
grpc_protobuf_build::CodeGen::new()
.include("proto")
.input("routeguide.proto")
.output_dir("generated")
.compile()
.unwrap();
这会调用 grpc_protobuf_build 箱的代码生成功能,并向其传递 routeguide.proto。我们已将此内容封装在一些代码中,以便仅在传递功能标志时运行,这样一来,只有在您需要时才会重新生成此内容。您现在无需运行此命令,因为我们已为您生成代码。
cargo build --bin routeguide-server --features regenerate_proto
运行 cargo build 时,build.rs 会将协议缓冲区定义编译到 generate/ 目录中,包括:
- 消息类型
Point和Feature的结构体定义。 - 我们需要为服务器实现的 Tonic 服务特征:
route_guide_server::RouteGuide。 - 我们将用于调用服务器的 gRPC-Rust 客户端类型:
route_guide_client::RouteGuideClient<T>。
您可以参阅 protoc-gen-rust-grpc 指南了解详情。
接下来,我们将在服务器上实现服务方法。
5. 实现服务
在 src/server/server.rs 中,我们可以通过 gRPC 的 include_generated_proto! 宏将生成的代码纳入作用域,并导入 RouteGuide 特征和 Point。
mod grpc_pb {
grpc::include_generated_proto!("generated", "routeguide");
}
use grpc_pb::{
route_guide_server::{RouteGuideServer, RouteGuide},
Point, Feature,
};
我们可以先定义一个结构来表示我们的服务,目前可以在 src/server/server.rs 上执行此操作:
#[derive(Debug)]
pub struct RouteGuideService {
features: Vec<Feature>,
}
现在,我们需要实现生成的代码中的 route_guide_server::RouteGuide 特征。
简单的一元 RPC
RouteGuideService 可实现我们的所有服务方法。服务器端的 get_feature 函数是完成主要工作的地方:它会从客户端获取 Point 消息,并从已知地点的列表中返回 Feature 消息中的相应位置信息。以下是该函数在 src/server/server.rs 中的实现:
#[tonic::async_trait]
impl RouteGuide for RouteGuideService {
async fn get_feature(&self, request: Request<Point>) -> Result<Response<Feature>, Status> {
println!("GetFeature = {:?}", request);
let requested_point = request.get_ref();
for feature in self.features.iter() {
if feature.location().latitude() == requested_point.latitude() {
if feature.location().longitude() == requested_point.longitude(){
return Ok(Response::new(feature.clone()))
};
};
}
Ok(Response::new(Feature::default()))
}
}
实现此方法后,我们还需要启动 gRPC 服务器,以便客户端能够实际使用我们的服务。将 main() 替换为以下内容。
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "[::1]:10000".parse().unwrap();
println!("RouteGuideServer listening on: {addr}");
let route_guide = RouteGuideService {
features: load(),
};
let svc = RouteGuideServer::new(route_guide);
Server::builder().add_service(svc).serve(addr).await?;
Ok(())
}
以下是 main() 中发生的情况,按步骤说明如下:
- 指定我们想要用于侦听客户端请求的端口
- 通过调用辅助函数
load()创建一个加载了功能的RouteGuideService。 - 使用我们创建的服务,通过
RouteGuideServer::new()创建 gRPC 服务器实例。 - 将我们的服务实现注册到 gRPC 服务器。
- 使用我们的端口详细信息在服务器上调用
serve(),以执行阻塞等待,直到进程被终止。
6. 创建客户端
在本节中,我们将探讨如何在 src/client/client.rs 中为我们的 RouteGuide 服务创建一个 Rust 客户端。
正如我们在 src/server/server.rs 中所做的那样,我们可以通过 gRPC 的 include_generated_proto! 宏将生成的代码引入作用域,并导入 RouteGuideClient 类型。
mod grpc_pb {
grpc::include_generated_proto!("generated", "routeguide");
}
use grpc_pb::{
route_guide_client::RouteGuideClient,
Point,
};
呼叫服务方式
在 gRPC-Rust 中,RPC 是异步的、非阻塞的,使用 Rust 的 async/await 语法来等待服务器的响应。
要调用服务方法,我们首先使用 Channel::builder() 创建一个 Channel,并指定服务器地址 (dns:///[::1]:10000) 和连接凭据 (LocalChannelCredentials)。然后,我们将通道传递给 RouteGuideClient::new() 以实例化我们的客户端:
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a new gRPC channel
let channel = Channel::builder(
"dns:///[::1]:10000",
Arc::new(LocalChannelCredentials::new()),
)
.build();
// Create a new client
let client = RouteGuideClient::new(channel);
}
在此函数中,RouteGuideClient::new() 将通用通道绑定到生成的客户端存根,该存根实现了我们 .proto 服务定义中定义的方法。
简单 RPC
调用简单的 RPC GetFeature 与调用本地方法一样简单直接。在 main() 中添加以下内容:
println!("*** SIMPLE RPC ***");
let point = proto!(Point {
latitude: 409_146_138,
longitude: -746_188_906,
});
let response = client
.get_feature(point)
.await
.expect("RPC error");
在 gRPC-Rust 中,我们将 protobuf 消息 Point 直接传递给 client.get_feature(point)。等待返回的 future 会直接得到 Feature 响应,无需进一步的方法调用。
接下来,打印响应中的字段:
println!(
"Response = Name = \"{}\", Latitude = {}, Longitude = {}",
response.name(),
response.location().latitude(),
response.location().longitude()
);
总而言之,客户端的 main() 函数应该如下所示:
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Create a new gRPC channel
let channel = Channel::builder(
"dns:///[::1]:10000",
Arc::new(LocalChannelCredentials::new()),
)
.build();
// 2. Instantiate the generated RouteGuideClient
let client = RouteGuideClient::new(channel);
println!("*** SIMPLE RPC ***");
let point = proto!(Point {
latitude: 409_146_138,
longitude: -746_188_906,
});
let response = client
.get_feature(point)
.await
.expect("RPC error");
println!(
"Response = Name = \"{}\", Latitude = {}, Longitude = {}",
response.name(),
response.location().latitude(),
response.location().longitude()
);
Ok(())
}
7. 试试看
要运行客户端和服务端,首先请确认两个二进制目标都已在 Cargo.toml 中定义:
[[bin]]
name = "routeguide-server"
path = "src/server/server.rs"
[[bin]]
name = "routeguide-client"
path = "src/client/client.rs"
然后,从当前工作目录执行以下命令:
- 在一个终端中运行服务器:
cargo run --bin routeguide-server
- 从另一个终端运行客户端:
cargo run --bin routeguide-client
您将看到如下输出,为清晰起见,时间戳已省略:
*** SIMPLE RPC ***
Response = Name = "Berkshire Valley Management Area Trail, Jefferson, NJ, USA", Latitude = 409146138, Longitude = -746188906
8. 后续步骤
- 继续学习 gRPC-Rust 使用入门(流式传输)Codelab。
- 探索官方 gRPC-Rust 代码库。
- 如需详细了解 gRPC 架构,请参阅核心概念。
- 在 gRPC.io 上查看 gRPC-Rust 文档。
9. 此 Codelab 的贡献者
- Cathy Zhao
- Lucio Franco
- Arvind Bright
- Nathaniel Ford