gRPC-Rust 使用入门

1. 简介

在此 Codelab 中,您将使用 gRPC-Rust 创建一个客户端和服务器,它们将构成用 Rust 编写的路线映射应用的基础。

在本教程结束时,您将拥有一个使用 gRPC 连接到远程服务器的客户端,以获取地图上特定坐标处的位置名称或邮寄地址。一个功能完善的应用可能会使用这种客户端-服务器设计来枚举或总结路线沿途的兴趣点。

该服务在 Protocol Buffers 文件中定义,该文件将用于为客户端和服务器生成样板代码,以便它们能够相互通信,从而节省您实现该功能的时间和精力。

生成的代码不仅能处理服务器与客户端之间复杂的通信,还能处理数据序列化和反序列化。

学习内容

  • 如何使用 Protocol Buffers 定义服务 API。
  • 如何使用自动代码生成功能基于 Protocol Buffers 定义构建基于 gRPC 的客户端和服务器。
  • 了解使用 gRPC 进行客户端-服务器通信。

本 Codelab 适用于刚开始使用 gRPC 或希望复习 gRPC 的 Rust 开发者,也适用于对构建分布式系统感兴趣的任何人。无需具备 gRPC 经验。

2. 准备工作

前提条件

请确保您已安装以下各项:

  • GCC。请按照此处的说明操作
  • Rust,版本 1.89.0。请按照此处的安装说明操作。

获取代码

为了避免您完全从头开始,本 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 方法时,客户端和服务器之间交换的数据结构消息类型 PointFeature。客户端在其 GetFeature 请求中向服务器提供地图坐标作为 Point,服务器则会回复相应的 Feature,其中描述了位于这些坐标处的任何内容。

此 RPC 方法及其消息类型都将在所提供源代码的 proto/route_guide.proto 文件中定义。

协议缓冲区通常称为 protobuf。如需详细了解 gRPC 术语,请参阅 gRPC 的核心概念、架构和生命周期

服务方法

我们先定义服务方法,然后定义消息类型 PointFeatureproto/routeguide.proto 文件具有名为 RouteGuideservice 结构,用于定义应用服务提供的一个或多个方法。

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;
}

数字 12message 结构中每个字段的唯一 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 文件生成代码,请参阅这些说明

生成的代码包含:

  • 消息类型 PointFeature 的结构体定义。
  • 我们需要实现的服务特征: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() 中发生的情况,分步说明如下:

  1. 指定我们想要用于侦听客户端请求的端口
  2. 通过调用辅助函数 load() 创建加载了特征的 RouteGuideService
  3. 使用我们创建的服务,通过 RouteGuideServer::new() 创建 gRPC 服务器的实例。
  4. 向 gRPC 服务器注册服务实现。
  5. 在服务器上使用我们的端口详细信息调用 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"

然后,从工作目录执行以下命令:

  1. 在一个终端中运行服务器:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-server 
  1. 从另一个终端运行客户端:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-client

您将看到如下输出(为清晰起见,省略了时间戳):

*** SIMPLE RPC ***

FEATURE: Name = "Berkshire Valley Management Area Trail, Jefferson, NJ, USA", Lat = 409146138, Lon = -746188906

8. 后续步骤