gRPC-Rust のスタートガイド

1. はじめに

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

このチュートリアルを完了すると、gRPC を使用してリモート サーバーに接続し、地図上の特定の座標にあるものの名前または郵便番号を取得するクライアントが作成されます。本格的なアプリケーションでは、このクライアント サーバー設計を使用して、ルート上のスポットを列挙または要約できます。

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

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

学習内容

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

この Codelab は、gRPC を初めて使用する Rust デベロッパー、gRPC の復習を希望するデベロッパー、分散システムの構築に関心のある方を対象としています。gRPC の経験は必要ありません。

2. 始める前に

前提条件

次のものがインストールされていることを確認します。

  • GCC。こちらの手順に沿って対応します。
  • Rust、バージョン 1.89.0。こちらのインストール手順に沿って操作します。

コードを取得する

この Codelab では、完全にゼロから始める必要がないように、アプリケーションのソースコードのスケルトンが用意されています。次の手順では、プロトコル バッファ コンパイラ プラグインを使用してボイラープレート 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 ファイルで定義されます。

Protocol Buffers は一般に protobuf と呼ばれます。gRPC の用語の詳細については、gRPC の コア コンセプト、アーキテクチャ、ライフサイクルをご覧ください。

サービス メソッド

まず、サービス メソッドを定義してから、メッセージ タイプ PointFeature を定義します。proto/routeguide.proto ファイルには、アプリケーションのサービスによって提供される 1 つ以上のメソッドを定義する RouteGuide という名前の service 構造があります。

RouteGuide 定義内に rpc メソッド GetFeature を追加します。前述のとおり、このメソッドは指定された座標セットから場所の名前または住所を検索するため、指定された Point に対して GetFeatureFeature を返すようにします。

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

数値 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. クライアント コードとサーバーコードを生成する

生成されたディレクトリの .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 トレイトを実装する必要があります。

Unary 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 サービスで定義された特定の方法を実装する生成コード スタブでラップします。

Simple 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. 試してみる

まず、クライアントとサーバーを実行するために、それらをバイナリ ターゲットとしてクレートに追加しましょう。これに伴い、Cargo.toml を編集して次の内容を追加する必要があります。

[[bin]]
name = "routeguide-server"
path = "src/server/server.rs"

[[bin]]
name = "routeguide-client"
path = "src/client/client.rs"

次に、作業ディレクトリから次のコマンドを実行します。

  1. 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. 次のステップ