gRPC-Rust のスタートガイド

1. はじめに

このコードラボでは、gRPC-Rust を使用して、Rust で記述されたルートマッピングアプリケーションの基盤となるクライアントとサーバーを作成します。

このチュートリアルの最後には、gRPC プロトコルの 公式 Rust 実装 を使用してリモート サーバーに接続し、地図上の特定の座標にある場所の名前または郵便番号を取得するクライアントが完成します。本格的なアプリケーションでは、このクライアント・サーバー設計を利用して、ルート沿いの興味深い地点を列挙したり、要約したりすることができるだろう。

このサービスはプロトコルバッファファイルで定義されており、クライアントとサーバーが相互に通信できるようにするための定型コードを生成するために使用されます。これにより、その機能の実装にかかる時間と労力を節約できます。

この生成されたコードは、サーバーとクライアント間の通信の複雑さだけでなく、データのシリアル化と逆シリアル化も処理します。

学習内容

  • Protocol Buffers を使用してサービス API を定義する方法。
  • 自動コード生成を使用して、プロトコルバッファ定義から gRPC ベースのクライアントを構築する方法。
  • gRPC を用いたクライアント・サーバー通信に関する理解。

このコードラボは、gRPC を初めて使う Rust 開発者、gRPC の復習をしたい開発者、あるいは分散システムの構築に興味のあるすべての人を対象としています。gRPC に関する事前の経験は必要ありません。

2. 始める前に

前提条件

以下のものがインストールされていることを確認してください。

コードを取得する

完全にゼロから始める必要がないように、このコードラボでは、アプリケーションのソースコードの骨組みが提供されており、それを完成させることができます。以下の手順では、プロトコルバッファコンパイラプラグインを使用して定型的な gRPC コードを生成する方法を含め、アプリケーションを完成させる方法を示します。

まず、コードラボの作業ディレクトリを作成し、そのディレクトリに移動します。

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 メソッド、およびリクエストとレスポンスのメッセージタイプを定義することです。お客様のサービスでは以下の内容を提供いたします。

  • サーバーが実装し、クライアントが呼び出す GetFeature と呼ばれる RPC メソッド。
  • Point および Feature は、GetFeature メソッドを使用する際にクライアントとサーバー間で交換されるデータ構造です。クライアントは、サーバーへのリクエストの GetFeature にマップ座標を Point として提供し、サーバーはそれらの座標に位置するものを記述する対応する Feature で応答します。

この RPC メソッドとそのメッセージタイプはすべて、提供されるソースコードのproto/routeguide.protoファイルで定義されます。

プロトコルバッファは一般的に protobuf として知られています。gRPC の用語の詳細については、gRPC の コア概念、アーキテクチャ、およびライフサイクル を参照してください。

サービス方法

まずサービスメソッドを定義し、次にメッセージタイプPointFeatureを定義しましょう。proto/routeguide.proto ファイルには、アプリケーションのサービスによって提供される 1 つ以上のメソッドを定義する 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/routeguide.protoファイルで、まずPointメッセージタイプを定義します。Pointは地図上の緯度経度座標のペアを表します。このコード演習では、座標には整数を使用してください。

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

番号12は、message構造内の各フィールドの一意の ID 番号です。

次に、Feature メッセージタイプを定義します。Feature は、Point で指定された場所にあるものの名​​前または郵便住所に string フィールドを使用します。

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クレートを使用します。

Cargo.toml では、[build-dependencies] の下に grpc-protobuf-build を追加します。

build.rsでは、grpc_protobuf_build::CodeGenを設定してproto/routeguide.protogenerated/ディレクトリにコンパイルします。重要な箇所は以下のとおりです。

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()で何が起こっているのかを、段階的に説明します。

  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 メッセージPointclient.get_feature(point)に直接渡します。返されたフューチャーを待機すると、追加のメソッド呼び出しを必要とせずに、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. サーバーを 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. このコードラボの貢献者

  • キャシー・チャオ
  • ルシオ・フランコ
  • アーヴィンド・ブライト
  • ナサニエル・フォード