開始使用 gRPC-Rust

1. 簡介

在本程式碼實驗室中,您將使用 gRPC-Rust 建立一個用戶端和伺服器,作為用 Rust 編寫的路由映射應用程式的基礎。

在本教學結束時,您將擁有一個客戶端,該客戶端使用 gRPC 協定的 官方 Rust 實作 連接到遠端伺服器,以取得地圖上特定座標位置的名稱或郵寄地址。一個功能齊全的應用程式可能會使用這種客戶端-伺服器設計來列舉或匯總沿途的興趣點。

該服務定義在一個 Protocol Buffers 檔案中,該檔案將用於生成客戶端和伺服器的樣板程式碼,以便它們可以相互通信,從而節省您實現該功能的時間和精力。

產生的程式碼不僅處理伺服器和客戶端之間通訊的複雜性,還處理資料序列化和反序列化。

課程內容

  • 如何使用 Protocol Buffers 定義服務 API。
  • 如何使用自動化程式碼產生技術,從 Protocol Buffer 定義建置基於 gRPC 的客戶端。
  • 瞭解使用 gRPC 進行客戶端-伺服器通訊。

本程式碼實驗室是針對剛接觸 gRPC 或希望複習 gRPC 的 Rust 開發人員,以及任何其他對建構分散式系統感興趣的人。無需具備 gRPC 使用經驗。

2. 事前準備

必要條件

請確保您已安裝以下軟體:

  • 海灣合作委員會。請依照此處的指示進行操作。
  • Git:安裝說明 在此
  • Rust,版本 1.88.0。請依照此處的安裝說明進行操作。

取得程式碼

為了避免您從零開始,本程式碼實驗室提供了應用程式原始碼的框架供您完成。以下步驟將向您展示如何完成應用程序,包括使用協定緩衝區編譯器插件產生樣板 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
  • 訊息類型 PointFeature 是使用 GetFeature 方法時客戶端和伺服器之間交換的資料結構。客戶端在其向伺服器發出的 GetFeature 請求中提供地圖座標作為 Point,伺服器回覆對應的 Feature,描述位於這些座標處的事物。

該 RPC 方法及其訊息類型都將在提供的原始程式碼的 proto/routeguide.proto 檔案中定義。

協定緩衝區通常被稱為 protobuf。有關 gRPC 術語的更多信息,請參閱 gRPC 的 核心概念、架構和生命週期

服務方法

我們先定義服務方法,然後定義訊息類型 PointFeatureproto/routeguide.proto 檔案有一個名為 RouteGuideservice 結構,它定義了應用程式服務提供的一個或多個方法。

RouteGuide 定義中新增 rpc 方法 GetFeature。如前所述,此方法將根據給定的座標集來尋找位置的名稱或位址,因此對於給定的 PointGetFeature 傳回 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;
}

數字 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. 產生客戶端和服務端程式碼

我們已在 generated/ 目錄中提供 .proto 檔案產生的程式碼,包括您在上方所做的所有新增項目。不過,我們想花點時間說明程式碼生成功能的運作方式。

我們的 .proto 檔案描述了客戶端或伺服器使用的所有結構和函數。我們使用 Cargo 建置腳本 (build.rs) 和 grpc-protobuf-build crate 來自動產生此程式碼。

Cargo.toml中,我們在[build-dependencies]下面加上grpc-protobuf-build

build.rs 中,我們配置 grpc_protobuf_build::CodeGenproto/routeguide.proto 編譯到 generated/ 目錄中。關鍵語句如下:

grpc_protobuf_build::CodeGen::new()
    .include("proto")
    .input("routeguide.proto")
    .output_dir("generated")
    .compile()
    .unwrap();

這會呼叫 grpc_protobuf_build Crate 的程式碼產生作業,並將 routeguide.proto 傳遞給該作業。我們已將這項功能包裝在程式碼中,只會在傳遞功能旗標時執行,因此只會在您想要時重新產生。我們已為您產生程式碼,因此您不必立即執行這項操作。

cargo build --bin routeguide-server --features regenerate_proto

執行 cargo build 時,build.rs 會將通訊協定緩衝區定義編譯到 generate/ 目錄,包括:

  • 訊息型別 PointFeature 的結構體定義。
  • 我們需要為伺服器實作的 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() 的運作方式:

  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_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"

然後,從目前工作目錄執行以下命令:

  1. 在一個終端機中運行伺服器:
cargo run --bin routeguide-server
  1. 從另一個終端機執行用戶端:
cargo run --bin routeguide-client

您會看到類似下列的輸出內容,為求明確起見,時間戳記已省略:

*** SIMPLE RPC ***
Response = Name = "Berkshire Valley Management Area Trail, Jefferson, NJ, USA", Latitude = 409146138, Longitude = -746188906

8. 後續步驟

9. 本程式碼研究室的協作者

  • Cathy Zhao
  • Lucio Franco
  • Arvind Bright
  • Nathaniel Ford