LoginSignup
6
2

More than 1 year has passed since last update.

RustでgRPCサーバーを動かしてみる

Last updated at Posted at 2022-12-18

こんにちは。

ついに来たか。アドベントカレンダーの季節ですねー。12月に入る前、娘が「アドベントカレンダー買ってきた!」と息巻いていて、「アドベントカレンダー……買う……とは……?」と思っていたら、本物の方のアドベントカレンダーの方でした。本物のアドベントカレンダーって本当にあるんだ。

この記事はNE株式会社の19日目のアドベントカレンダーです。NE株式会社はHamee株式会社から今年の8月に分社しました。そう考えると今年も色々ありましたな。「お疲れさまでした」にはまだ早いけれど、お疲れさまでした。酒飲みながら書くことにします。

乾杯!

RustでgRPCサーバーを動かしてみる

Rustを実務に使ったことがない、gRPCも実務に使ったことがない、ということは二つくっつけて動かしてみれば両方とも勉強になるのでは? というかなり頭の悪い動機でRustでgRPCサーバを動かしてみたいと思います。 hello world と認証のサンプルを動かすことができるのが今の夢です。それではgRPCの公式でRustのサンプルでも持ってきましょう!

image.png
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の中にどうぞ>>>
proto/helloworld.proto
syntax = "proto3";
package helloworld;

service Greeter {
    rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
   string name = 1;
}

message HelloReply {
    string message = 1;
}
Cargo.toml
[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"
build.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_build::compile_protos("proto/helloworld.proto")?;
    Ok(())
}
src/server.rs
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つ書き換えます>>>
proto/helloworld.proto
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) {}
}
src/server.rs
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もっと勉強してこ。

それではよいクリスマス&お年を!

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2