こんにちは。
ついに来たか。アドベントカレンダーの季節ですねー。12月に入る前、娘が「アドベントカレンダー買ってきた!」と息巻いていて、「アドベントカレンダー……買う……とは……?」と思っていたら、本物の方のアドベントカレンダーの方でした。本物のアドベントカレンダーって本当にあるんだ。
この記事はNE株式会社の19日目のアドベントカレンダーです。NE株式会社はHamee株式会社から今年の8月に分社しました。そう考えると今年も色々ありましたな。「お疲れさまでした」にはまだ早いけれど、お疲れさまでした。酒飲みながら書くことにします。
乾杯!
RustでgRPCサーバーを動かしてみる
Rustを実務に使ったことがない、gRPCも実務に使ったことがない、ということは二つくっつけて動かしてみれば両方とも勉強になるのでは? というかなり頭の悪い動機でRustでgRPCサーバを動かしてみたいと思います。 hello world
と認証のサンプルを動かすことができるのが今の夢です。それではgRPCの公式でRustのサンプルでも持ってきましょう!
https://grpc.io/docs/languages/
うぉうぉーい、公式対応ないじゃーーーん!
hyperium/tonic で動かす準備
いかがでしたでしょうか。これで詰んだと思ったんですが、今をときめくRustの懐は浅くないはずだと思いなおしたところ、実務に耐えるCrateがいくつか存在するようです。hyperium/tonic がその一つで、Starが6.4kなので、かなり利用されているようですね。
内容を確認するとhello worldのチュートリアル がありました。それをひとまずそのまま動かすことにします。下記のような環境で動かしています。
$ masakuni@mi-air % sw_vers
ProductName: macOS
ProductVersion: 12.6
BuildVersion: 21G115
$ masakuni@mi-air % rustc --version
rustc 1.63.0
README.mdに書かれているんですが、いきなり Cargo new
する前にgRPCでデータを乗せるためにプロトコルバッファが扱える必要があります。プロトコルバッファはGoogleが策定した、構造化された多くの種類のデータをシリアライズして、低コストで扱えるようにしたフォーマットです。 .proto
というファイルでデータの定義があらかじめ書かれている必要があります。
tonicではプロトコルバッファのコンパイラである protoc
が必要とのことであらかじめ下記のようにインストールしておきました。
$ brew install protobuf
これを入れとかないと先のほうで下記のようなエラーがでます。なんで知ってるかって言うと、いきなり Cargo new
して動かなかったからです。READMEに書いてあるものはちゃんと読もう!
error: failed to run custom build command for `helloworld-tonic v0.1.0 (/path/to/work/projects/helloworld-tonic)`
Caused by:
process didn't exit successfully: `/path/to/work/projects/helloworld-tonic/target/debug/build/helloworld-tonic-6f3e256272048e89/build-script-build` (exit status: 101)
--- stdout
cargo:rerun-if-changed=proto/helloworld.proto
cargo:rerun-if-changed=proto
--- stderr
thread 'main' panicked at 'Could not find `protoc` installation and this build crate cannot proceed without
this knowledge. If `protoc` is installed and this crate had trouble finding
it, you can set the `PROTOC` environment variable with the specific path to your
installed `protoc` binary.You could try running `brew install protobuf` or downloading it from https://github.com/protocolbuffers/protobuf/releases
For more information: https://docs.rs/prost-build/#sourcing-protoc
', /path/to/masakuni/.cargo/registry/src/github.com-1ecc6299db9ec823/prost-build-0.11.4/src/lib.rs:1296:10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
gRPCのサーバーのコード
$ cargo new helloworld-tonic
$ cd helloworld-tonic
正直あとはチュートリアルのコードそのままで動きました。ファイルの位置が若干分かりにくかったので最終的に下記のような構造になりました。
helloworld-tonic
├── build.rs
├── Cargo.lock
├── Cargo.toml
├── proto
│ └── helloworld.proto
├── src
│ ├── main.rs
│ └── server.rs
└── target
<<<ざーっと書いたコードはdetailsの中にどうぞ>>>
syntax = "proto3";
package helloworld;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
[package]
name = "helloworld-tonic"
version = "0.1.0"
edition = "2021"
[[bin]] # Bin to run the HelloWorld gRPC server
name = "helloworld-server"
path = "src/server.rs"
[[bin]] # Bin to run the HelloWorld gRPC client
name = "helloworld-client"
path = "src/client.rs"
[dependencies]
tonic = "0.8"
prost = "0.11"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
[build-dependencies]
tonic-build = "0.8"
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("proto/helloworld.proto")?;
Ok(())
}
use tonic::{transport::Server, Request, Response, Status};
use hello_world::greeter_server::{Greeter, GreeterServer};
use hello_world::{HelloReply, HelloRequest};
pub mod hello_world {
tonic::include_proto!("helloworld");
}
#[derive(Debug, Default)]
pub struct MyGreeter {}
#[tonic::async_trait]
impl Greeter for MyGreeter {
async fn say_hello(
&self,
request: Request<HelloRequest>,
) -> Result<Response<HelloReply>, Status> {
println!("Got a request: {:?}", request);
let reply = hello_world::HelloReply {
message: format!("Hello {}!", request.into_inner().name).into(),
};
Ok(Response::new(reply))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "[::1]:50051".parse()?;
let greeter = MyGreeter::default();
Server::builder()
.add_service(GreeterServer::new(greeter))
.serve(addr)
.await?;
Ok(())
}
ここまで書くとgRPCサーバーが動くようになるようです。下記コマンドで実行します。
$ cargo run --bin helloworld-server
Finished dev [unoptimized + debuginfo] target(s) in 4.76s
Running `target/debug/helloworld-server`
GreeterServer listening on [::1]:50051 # localhostの50051ポートで待ち受けてるよー
それではcurlコマンドのようにgRPCへデータを送れる grpcurl コマンド(brewで入れられます)でリクエストを送ってみます。
$ grpcurl -plaintext -import-path ./proto -proto helloworld.proto -d '{"name": "Masakuni"}' '[::]:50051' helloworld.Greeter/SayHello
{
"message": "Hello Masakuni!"
}
返ってきた。
認証ってどうやるの?
tonicにはexamplesがいくつも付属していて、実装のサンプルとして利用できるようです。認証の例もあったので、下記のようにファイルを書き換えると、それっぽく動くようです。JWTとかと合わせて利用するのかな。
<<<ファイルを2つ書き換えます>>>
syntax = "proto3";
package grpc.examples.unaryecho;
// EchoRequest is the request for echo.
message EchoRequest {
string message = 1;
}
// EchoResponse is the response for echo.
message EchoResponse {
string message = 1;
}
// Echo is the echo service.
service Echo {
// UnaryEcho is unary echo.
rpc UnaryEcho(EchoRequest) returns (EchoResponse) {}
}
pub mod pb {
tonic::include_proto!("grpc.examples.unaryecho");
}
use pb::{EchoRequest, EchoResponse};
use tonic::{metadata::MetadataValue, transport::Server, Request, Response, Status};
type EchoResult<T> = Result<Response<T>, Status>;
#[derive(Default)]
pub struct EchoServer;
#[tonic::async_trait]
impl pb::echo_server::Echo for EchoServer {
async fn unary_echo(&self, request: Request<EchoRequest>) -> EchoResult<EchoResponse> {
let message = request.into_inner().message;
Ok(Response::new(EchoResponse { message }))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "[::1]:50051".parse().unwrap();
let server = EchoServer::default();
let svc = pb::echo_server::EchoServer::with_interceptor(server, check_auth);
Server::builder().add_service(svc).serve(addr).await?;
Ok(())
}
fn check_auth(req: Request<()>) -> Result<Request<()>, Status> {
let token: MetadataValue<_> = "Bearer some-secret-token".parse().unwrap();
match req.metadata().get("authorization") {
Some(t) if token == t => Ok(req),
_ => Err(Status::unauthenticated("No valid auth token")),
}
}
サーバーを起動し直します。
$ cargo run --bin helloworld-server
Compiling helloworld-tonic v0.1.0 (/path/to/helloworld-tonic)
Finished dev [unoptimized + debuginfo] target(s) in 12.74s
Running `target/debug/helloworld-server`
トークンが合ってれば合ってるか確認します。
$ grpcurl -plaintext -import-path ./proto -proto helloworld.proto \
> -rpc-header 'authorization: Bearer some-secret-token' \
> -d '{"message": "nemui"}' \
> '[::]:50051' grpc.examples.unaryecho.Echo/UnaryEcho
{
"message": "nemui"
}
おお。それではトークンが間違っていると?
$ grpcurl -plaintext -import-path ./proto -proto helloworld.proto \
> -rpc-header 'authorization: Bearer bad-himitsuno-token' \
> -d '{"message": "nemui"}' \
> '[::]:50051' grpc.examples.unaryecho.Echo/UnaryEcho
ERROR:
Code: Unauthenticated
Message: No valid auth token
おお、ちゃんとエラー返ってきた。
まとめ
この程度だったら、例えばRESTと比較してgRPCの優位性は特になく、サーバーのリソースを活かすとか、双方向ストリーミングをするとか、そういった技術的なメリットがあるかどうかで選定されるものと思いますが、 .proto
で定義と実装が結びついているところなど、理解が追いつけばすっきりしたインターフェースなんだろな、と感じました。
今回は作ってませんけど、tonicではserverだけじゃなくgRPCのclientももちろん作れるので、サーバー&クライアント、両方ともRustで行けそうです。公式にはないのなんでかな。Rustもっと勉強してこ。
それではよいクリスマス&お年を!