🦁

RustでgRPCサーバーを立ててみた

2022/11/01に公開

はじめに

はじめましてsweeepの五十嵐です。
sweeepではマイクロサービス構成の中のgRPCサーバーとしてGoを使っています。
個人的にRustに興味があったので、今回はgRPCサーバーをRustでつくってみようと思います。

Rustとは

  • C/C++に匹敵するパフォーマンスやメモリ安全性を両立している
  • GCの代わりに所有権モデルという独自のコンセプトを採用している(コンパイルが通る限りメモリ安全性が担保されている)
  • 低レイヤ以外にWebアプリケーションのバックエンド開発にも使える
  • モダンな言語機能が一通り入っている

低レイヤのイメージが強いRustですが、OSからWebアプリケーションまで幅広く実装できます。
メモリの安全性を言語仕様として保証してくれる上に、コンパイラのメッセージがとても親切なのが魅力的でした。

Hello world

https://github.com/hyperium/tonic/blob/master/examples/helloworld-tutorial.md
公式のチュートリアルに従ってHello worldしてみます(今回環境構築は飛ばさせていただきます)
gRPCのライブラリではtonicがデファクトスタンダードのようです。

それでは、実際にプロジェクトを作成して動作を確認していきます。

$ cargo new helloworld-tonic
$ cd helloworld-tonic

protocol bufferでAPIを定義します。

test.proto
syntax = "proto3";
package helloworld;

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

message HelloRequest {
   string name = 1;
}

message HelloReply {
    string message = 1;
}

Cargo.toml に依存関係を記載します。

cargo.toml
[package]
name = "helloworld-tonic"
version = "0.1.0"
edition = "2021"

[dependencies]
tonic = "0.8"
prost = "0.11"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }

[build-dependencies]
tonic-build = "0.8"

プロジェクトのルートで、build.rsファイルを作成し、次のコードを追加します。

builder.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_build::compile_protos("proto/helloworld.proto")?;
    Ok(())
}

cargo buildでコードを自動生成します。
tonic-buildの中ではprostというcrateを介して、prost-buildにコード生成の処理を委譲しているようです。
そしてprost-buildにはprotocコマンドがバイナリとして埋め込まれているので、protocコマンドのインストールが不要です。

buildに成功するとtarget配下にコードが生成されます。

サーバーを実装します。

main.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(())
}

cargo runでサーバーを起動した後に
grpcurlを使用して以下のように送信リクエストを試します。

$ grpcurl -plaintext -import-path ./proto -proto helloworld.proto -d '{"name": "Tonic"}' '[::]:50051' helloworld.Greeter/SayHello

以下のようにレスポンスが返ってきたら成功です🦀

{
  "message": "Hello Tonic!"
}

さいごに

途中ハマることなくすんなりgRPCサーバーを立てることができました。
個人的に低レイヤにも興味あるのでもっとRustを触っていきたいと思いました。

最後に、弊社では絶賛エンジニア募集中なので、気になる方は以下を覗いてみてください!
https://www.wantedly.com/companies/sweeep/projects

Discussion