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 Crate 的程式碼產生作業,並將 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 (串流)」程式碼研究室。
- 探索官方 gRPC-Rust 存放區。
- 如要進一步瞭解 gRPC 架構,請參閱「核心概念」。
- 請參閱 gRPC.io 上的 gRPC-Rust 說明文件。
9. 本程式碼研究室的協作者
- Cathy Zhao
- Lucio Franco
- Arvind Bright
- Nathaniel Ford