tonicを使ってgRPCサーバーを立ててみる
作成日:
gRPCサーバー+grpc-webをバックエンドにしてWiki的なのをつくろうという気持ちがあるので、そのための実験をしています。今回はgRPCサーバーをRustでつくれるかの調査です。
ライブラリ選定
Rustで、Hello Worldを卒業した?次は、gRPCだよね! | by FUJITA Tomonori | nttlabs | Medium というRust教な記事があり、ここで tonic が推されています。
私は別にRust教信者ではないですが、Rustだけで完結していると環境構築が楽そうなので、とりあえずtonicを試してみることにしました。‘grpc’ search // Lib.rs でもtonicが一番人気っぽいですし、まあたぶんよくできているのでしょう。
Hello world
tonic/helloworld-tutorial.md at master · hyperium/tonicに従ってHelloWorldします。
環境準備
Rust 2021 Editionに依存しているのでRust 1.56以降が必要というのはなかなか面白いですね。
エディタにはVSCode、補完にはrust-analyzerを使います。ここで注意点なのですが、READMEには
If you’re using rust-analyzer we recommend you set “rust-analyzer.cargo.loadOutDirsFromCheck”: true to correctly load the generated code.
とあるものの、この通りにやると “unknown configuration setting” とVSCodeに言われてしまいます。rust-analyzerのIssueによると、この rust-analyzer.cargo.loadOutDirsFromCheck
設定は rust-analyzer.cargo.runBuildScripts
に改名された上に既定でオンになるようになったらしいです。つまり、特に何の設定もしなくてもよいということですね。
proto/sinanoki.proto
protocol bufferでAPI定義を書きます。なお、このsinanokiというのは単なる(私が作ろうとしているものの)プロジェクト名なので意味は無いです。
syntax = "proto3";
package sinanoki;
service PageManager {
rpc GetPage(GetPageRequest) returns (GetPageResponce);
}
message GetPageRequest {
string path = 1;
}
message GetPageResponce {
string markdown = 1;
}
cargo.toml
[package]
name = "sinanoki-backend"
version = "0.1.0"
authors = ["suzusime <***@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tonic = "0.6"
prost = "0.9"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
[build-dependencies]
tonic-build = "0.6"
言われる通りにdependenciesの設定を追加します。今回はサーバーだけ(複数の実行ファイルはいらない)なので、binまわりの設定は飛ばしてserver.rsの代わりにmain.rsに書くことにします。
build.rs
ビルドに対して特殊な処理が必要になるので、ルートフォルダにbuild.rsを置きます。
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("./proto/sinanoki.proto")?;
Ok(())
}
src/main.rs
今までの設定をしてcargo build
すると、protocol bufferに書いたものに対応したサーバー/クライアントの雛型となるRustのコードがtarget以下に生成されるようになります。これを利用してサーバーを実装します。
例えば私の場合は target/debug/build/sinanoki-backend-1664b6d9540c1c9f/out/sinanoki.rs
に雛型コードが吐かれました。この雛型コードの中に
#[async_trait]
pub trait PageManager: Send + Sync + 'static {
async fn get_page(
&self,
request: tonic::Request<super::GetPageRequest>,
) -> Result<tonic::Response<super::GetPageResponce>, tonic::Status>;
}
というのがあるので、これをコピペしたものを使い、以下のようなコードをsrc/main.rs
に書きます。
use sinanoki::page_manager_server::{PageManager, PageManagerServer};
use sinanoki::{GetPageRequest, GetPageResponce};
use tonic::{transport::Server, Request, Response, Status};
pub mod sinanoki {
tonic::include_proto!("sinanoki"); // The string specified here must match the proto package name
}
#[derive(Debug, Default)]
pub struct MyPageManager {}
#[tonic::async_trait]
impl PageManager for MyPageManager {
async fn get_page(
&self,
request: Request<GetPageRequest>,
) -> Result<Response<GetPageResponce>, Status> {
println!("Got a request: {:?}", request);
let reply = GetPageResponce {
markdown: format!("you requested a file of {}", request.into_inner().path).into(), // We must use .into_inner() as the fields of gRPC requests and responses are private
};
Ok(Response::new(reply))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "[::1]:50051".parse()?;
let manager = MyPageManager::default();
Server::builder()
.add_service(PageManagerServer::new(manager))
.serve(addr)
.await?;
Ok(())
}
実行
あとは
$ cargo run
して、
$ grpcurl -plaintext -import-path ./proto -proto ./proto/sinanoki.proto -d '{"path": "/index.md"}' localhost:50051 sinanoki.PageManager/GetPage
{
"markdown": "you requested a file of /index.md"
}
で動作が確認できました。
所感
どうせ何かハマるだろうと思っていましたが、すんなり動きました。
困ったところを挙げるならば、(tonicに限らず)Rustの補完があまり気持ちよく動いてくれないことでしょうか。単にバグっぽいところ(implしようとしたときに勝手に出してくれるコードが明らかにおかしい)を除いても適切な補完候補がなかなか出てくれなくて、少し厳しいです。おそらく私の設定が悪いんだろうなと思っているのですが……。