1. 简介
在此 Codelab 中,您将使用 gRPC-Rust 创建一个客户端和服务器,它们将构成用 Rust 编写的路线映射应用的基础。
在本教程结束时,您将拥有一个使用 gRPC 连接到远程服务器的客户端,以获取地图上特定坐标处的位置名称或邮寄地址。一个功能完善的应用可能会使用这种客户端-服务器设计来枚举或总结路线沿途的兴趣点。
该服务在 Protocol Buffers 文件中定义,该文件将用于为客户端和服务器生成样板代码,以便它们能够相互通信,从而节省您实现该功能的时间和精力。
生成的代码不仅能处理服务器与客户端之间复杂的通信,还能处理数据序列化和反序列化。
学习内容
- 如何使用 Protocol Buffers 定义服务 API。
- 如何使用自动代码生成功能基于 Protocol Buffers 定义构建基于 gRPC 的客户端和服务器。
- 了解使用 gRPC 进行客户端-服务器通信。
本 Codelab 适用于刚开始使用 gRPC 或希望复习 gRPC 的 Rust 开发者,也适用于对构建分布式系统感兴趣的任何人。无需具备 gRPC 经验。
2. 准备工作
前提条件
请确保您已安装以下各项:
获取代码
为了避免您完全从头开始,本 Codelab 提供了一个应用源代码框架供您完成。以下步骤将展示如何完成应用,包括使用 Protocol Buffer 编译器插件生成样板 gRPC 代码。
首先,创建 Codelab 工作目录并进入该目录:
mkdir grpc-rust-getting-started && cd grpc-rust-getting-started
下载并解压缩 Codelab:
curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/v1.tar.gz \
| tar xvz --strip-components=4 \
grpc-codelabs-1/codelabs/grpc-rust-getting-started/start_here
或者,您也可以下载仅包含 Codelab 目录的 .zip 文件,然后手动将其解压缩。
如果您想跳过输入实现代码的步骤,可以在 GitHub 上找到完整的源代码。
3. 定义服务
第一步是使用协议缓冲区定义应用的 gRPC 服务、RPC 方法以及请求和响应消息类型。您的服务将提供:
- 一种名为
GetFeature
的 RPC 方法,由服务器实现并由客户端调用。 - 使用
GetFeature
方法时,客户端和服务器之间交换的数据结构消息类型Point
和Feature
。客户端在其GetFeature
请求中向服务器提供地图坐标作为Point
,服务器则会回复相应的Feature
,其中描述了位于这些坐标处的任何内容。
此 RPC 方法及其消息类型都将在所提供源代码的 proto/route_guide.proto
文件中定义。
协议缓冲区通常称为 protobuf。如需详细了解 gRPC 术语,请参阅 gRPC 的核心概念、架构和生命周期。
服务方法
我们先定义服务方法,然后定义消息类型 Point
和 Feature
。proto/routeguide.proto
文件具有名为 RouteGuide
的 service
结构,用于定义应用服务提供的一个或多个方法。
在 RouteGuide
定义中添加 rpc
方法 GetFeature
。如前所述,此方法将根据给定的坐标集查找某个位置的名称或地址,因此让 GetFeature
为给定的 Point
返回 Feature
:
service RouteGuide {
// Definition of the service goes here
// Obtains the feature at a given position.
rpc GetFeature(Point) returns (Feature) {}
}
这是一元 RPC 方法:一种简单 RPC,其中客户端向服务器发送请求并等待响应返回,就像本地函数调用一样。
消息类型
在源代码的 proto/route_guide.proto
文件中,首先定义 Point
消息类型。Point
表示地图上的纬度-经度坐标对。在此 Codelab 中,请使用整数作为坐标:
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. 生成客户端和服务器代码
我们已为您提供生成目录中 .proto
文件中的生成代码。
与任何项目一样,我们需要考虑代码所需的依赖项。对于 Rust 项目,依赖项将位于 Cargo.toml
中。我们已在 Cargo.toml
文件中列出了必要的依赖项。
如果您想了解如何自行从 .proto
文件生成代码,请参阅这些说明。
生成的代码包含:
- 消息类型
Point
和Feature
的结构体定义。 - 我们需要实现的服务特征:
route_guide_server::RouteGuide
。 - 我们将用于调用服务器的客户端类型:
route_guide_client::RouteGuideClient<T>
。
接下来,我们将在服务器端实现 GetFeature
方法,以便客户端发送请求时,服务器可以回复答案。
5. 实现服务
在 src/server/server.rs
中,我们可以通过 gRPC 的 include_generated_proto!
宏将生成的代码纳入作用域,并导入 RouteGuide
特征和 Point
。
mod grpc_pb {
grpc::include_generated_proto!("generated", "routeguide");
}
pub 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()))
}
}
在该方法中,使用给定 Point
的相应信息填充 Feature
对象,然后返回该对象。
实现此方法后,我们还需要启动 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_code!
宏将生成的代码纳入作用域,并导入 RouteGuideClient
类型。
mod grpc_pb {
grpc::include_generated_proto!("generated", "routeguide");
}
use grpc_pb::{
route_guide_client::RouteGuideClient,
Point,
};
调用服务方法
在 gRPC-Rust 中,RPC 以阻塞/同步模式运行,这意味着 RPC 调用会等待服务器响应,并返回响应或错误。
如需调用服务方法,我们首先需要创建一个与服务器通信的渠道。我们通过以下方式创建此对象:先创建一个端点,连接到该端点,然后传递连接到 RouteGuideClient::new()
时创建的渠道:
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create endpoint to connect to
let endpoint = Endpoint::new("http://[::1]:10000")?;
let channel = endpoint.connect().await?;
// Create a new client
let mut client = RouteGuideClient::new(channel);
Ok(())
}
在此函数中,当我们创建客户端时,会使用生成的代码存根(用于实现 .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(Request::new(point))
.await?.into_inner();
Ok(())
如您所见,我们对之前获得的桩调用了该方法。在我们的方法参数中,我们创建并填充了一个请求协议缓冲区对象(在本例中为 Point
)。如果调用未返回错误,则我们可以从第一个返回值中读取服务器的响应信息。
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>> {
//Create endpoint to connect to
let endpoint = Endpoint::new("http://[::1]:10000")?;
let channel = endpoint.connect().await?;
// Create a new client
let mut client = RouteGuideClient::new(channel);
println!("*** SIMPLE RPC ***");
let point = proto!(Point{
latitude: 409_146_138,
longitude: -746_188_906
});
let response = client
.get_feature(Request::new(point))
.await?.into_inner();
println!("Response = Name = \"{}\", Latitude = {}, Longitude = {}",
response.name(),
response.location().latitude(),
response.location().longitude());
Ok(())
}
7. 试试看
首先,为了运行客户端和服务器,我们将它们作为二进制目标添加到 crate 中。我们需要相应地修改 Cargo.toml
并添加以下内容:
[[bin]]
name = "routeguide-server"
path = "src/server/server.rs"
[[bin]]
name = "routeguide-client"
path = "src/client/client.rs"
然后,从工作目录执行以下命令:
- 在一个终端中运行服务器:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-server
- 从另一个终端运行客户端:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-client
您将看到如下输出(为清晰起见,省略了时间戳):
*** SIMPLE RPC *** FEATURE: Name = "Berkshire Valley Management Area Trail, Jefferson, NJ, USA", Lat = 409146138, Lon = -746188906