Tonicを用いてgRPC開発する際、サーバープロセスの構築以外に、protobufを適切に組込むことが必要になります。
Tonicの場合は、
PROST!を利用しており、protocコマンドで生成するフローとやや異なります。
なお、本記事のロジックと同等のコード生成CLIツール、 Alainをオープンソースで配布しています。
protoインクルードの構造
gRPC開発では、protobufの定義と各言語の変数との対応を意識することが重要です。
以下のようなprotobufのコードを組込む例を考えます。
syntax = "proto3";
package example.duck;
service ExampleDuck {
rpc LameDuck(Dummy) returns (Dummy) {}
}
message Dummy {}
まず、マクロを用いてprotoをインクルードします。
pub mod example {
pub mod duck {
tonic::include_proto!("example.duck");
}
}
この際、mod
の階層をprotobufのpackage名前空間に揃えて定義することが重要です。
構造を間違えている場合、
superがネストし過ぎるコードが出力されるといった非常に分かりにくいエラーに直面します。
proto定義の参照
インクルードしたproto定義は、mod構造に沿ってrustプロジェクトの名前空間にアサインされます。
先ほどの例では、crate::example::duck::Dummy
のような名前でインポートされます。
なお、rpcはcrate::example::duck::lame_duck()
のようにスネークケースになります。
この他、サーバーなどの構造体もツールの命名規約に沿って自動生成されています。target/
ディレクトリに自動生成されたファイルがあり、個別のケースはコードを確認することになります。
- example::duck::LameDuckServer
- example::duck::LameDuckClient
- example::duck::LameDuckService
サーバー実装では、protoに定義したrpcをすべて実装しないとコンパイル時エラーになります。
RequestとResponseの構造
Requestは、tonic::Request<example::duck::Dummy>
のようにprotobufメッセージをtonic::Request
でラップしたものです。
メッセージはinto_inner()
で取り出せます。
またResponseは、Result<tonic::Response<example::duck::Dummy, tonic::Status>
のように、tonic::Response
またはtonic::Status
を返すResult型です。
ビルドスクリプト
tonicは、cargo build
実行時にprotobufをrustコードにコンパイルするフローを想定しています。
そのため、以下のようなbuild.rs
をプロジェクトルートに置きます。
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("proto/example_duck.proto")?;
Ok(())
}
なお、protobufコンパイルにはtonic-build
クレートが必要であるため、Cargo.tomlのbuild-dependenciesに追加しておきます。
[build-dependencies]
tonic-build = { version = "0.4", features = ["prost"] }
複数のserviceを提供するServer
gRPCではservice
がrpc
をまとめる単位となり、proto内に複数のservice
を定義できます。
tonicはservice
を同名のトレイトに対応させています。
各トレイトは分割実装できないためservice
内のrpc
の数が増えると、長大なトレイト定義をimpl
することになります。
service
を論理的に分割できる場合には、各トレイト実装を別のファイルに定義できます。
各サービスはエントリポイントのServer構築プロセスで、
add_service()を用いて追加します。
add_service()
メソッドは単一のサーバーに複数指定できるため、単一ポートに複数のサービスを統合できます。
Chuma Takahiro