26
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

gRPC-webを使って、rustのgRPCサーバーとhello worldする。

Last updated at Posted at 2020-06-11

以下の技術スタックでhello worldをするようなやつを作りました。

  • フロント
    gRPC-web、TypeScript

  • サーバー
    gRPC、rust、docker

  • プロキシ
    envoy、docker

grpc.gif
コードはこちらにあります。
こんな感じのディレクトリ構成になっており、フロント以外はdocker-composeで動かすようにしました。

.
├── README.md
├── docker-compose.yml
├── front
│   ├── README.md
│   ├── gen.sh
│   ├── node_modules
│   ├── package-lock.json
│   ├── package.json
│   ├── public
│   ├── src
│   └── tsconfig.json
├── proxy
│   ├── Dockerfile
│   └── envoy.yaml
└── server
    ├── Cargo.lock
    ├── Cargo.toml
    ├── Dockerfile
    ├── README.md
    ├── build.rs
    ├── proto
    ├── src
    └── target

gRPCについて

gRPCサーバーを実装していく際の流れのイメージはこんな感じです。

1. プロトコル定義ファイル(Protocol Buffers)を作成

Protocol Buffersは型が書けるスキーマ言語です。各言語で共通して扱うことができるインタフェースのような感じです。

今回のただhello worldするだけのprotoファイルはこちらです。SayHelloというリモートプロシージャ(関数)はHelloRequestを引数に取り、HelloReplyを返すというインタフェースを定義しています。

server/proto/helloworld.proto

syntax = "proto3";

package helloworld;

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

message HelloRequest { string name = 1; }

message HelloReply { string message = 1; }

= 1 はフィールド番号と呼ばれるものです。Protocol Buffersでは、フィールド名ではなくフィールド番号を使ってそのメッセージの構造を保存します。そのためこの例だと、HelloRequest、HelloReplyともに、フィールドが一つしかないので = 1 のみですが、フィールドが増えていけば、 = 2 , = 3 と増えていきます。

2. プロトコル定義ファイルから各言語のインタフェースとなるコードを自動生成

上記のprotoファイルはそのままだと、どの言語もインタフェースとして利用することはできません。
protocというツールを使うことで上記のprotoファイルを元に、各言語でのインタフェースのコードを自動生成することができます。

3. 実装を書く

2で作成したインタフェースにしたがって実装を書いていきます。
この インタフェースにしたがって という部分は言語によって実現の仕方が異なっており、

  • pythonであれば自動生成されたコードを継承する形でクラスを実装する
  • nodeであれば、自動生成されたコードに実装の関数を引数として渡す

みたいな感じになってるみたいです。

rustでのgRPCについて

今回、rustでgRPCサーバーを作るにあたって、tonicを利用しました。

tonicの特徴として、protocコマンドを直接使う必要がないということが挙げられます。
他の言語でgRPCサーバーを実装する際はprotocコマンドを使ってコードを自動生成しますが、tonicではtonic-buildを用いてコードを自動生成します。そのため、protocコマンドのインストールも不要です。

具体的にはこんな感じです。

定義したprotoファイルを読み込むビルドスクリプトを追加します。これで、cargo buildを行った時にrustのコードが自動生成されます。コードはtargetディレクトリ配下に生成されます。

server/build.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_build::compile_protos("proto/helloworld.proto")?;
    Ok(())
}
  • 自動生成されたコード
    スクリーンショット 2020-06-12 7.09.50.png

このtargetディレクトリ配下に生成されたコードは、こんな感じで参照できます。

server/src/server.rs
use tonic::{transport::Server, Request, Response, Status};

pub mod hello_world {
    tonic::include_proto!("helloworld");
}

use hello_world::{
    greeter_server::{Greeter, GreeterServer},
    HelloReply, HelloRequest,
};

あとはこのインタフェースや型にしたがって、実装を書きます。
引数として、何か名前が渡されていれば Hello <渡された名前>! 、何も名前が渡されていない場合は Hello World! と返すような実装にしました。

server/src/server.rs
# [derive(Default)]
pub struct MyGreeter {}

# [tonic::async_trait]
impl Greeter for MyGreeter {
    async fn say_hello(
        &self,
        request: Request<HelloRequest>,
    ) -> Result<Response<HelloReply>, Status> {
        let mut name = request.into_inner().name;

        if name.len() == 0 {
            name = "World".to_string();
        }

        let reply = hello_world::HelloReply {
            message: format!("Hello {}!", name),
        };
        Ok(Response::new(reply))
    }
}

cargo run --bin helloworld-server でgRPCサーバーを起動したうえで、grpcurlを使えばちゃんとレスポンスが返ってくることが確認できます。
スクリーンショット 2020-06-11 22.00.06.png

gRPC-webについて

フロントの土台となるコードはCRAを使っています。

$ npx create-react-app front --template redux-typescript

gRPC-webの実装は、公式のhellowWorldのチュートリアルを参考に実装しました。
gRPC-webもサーバーのときと同様に、protoファイルを元にインタフェースのコードを自動生成し、それを活用します。

front/gen.sh

# !/bin/sh

parentdir=$(dirname `pwd`)
protodir="${parentdir}/server/proto"
outputpath="${parentdir}/front/src/proto"

protoc -I=${protodir} ${protodir}/helloworld.proto \
  --js_out=import_style=commonjs:${outputpath} \
  --grpc-web_out=import_style=typescript,mode=grpcwebtext:${outputpath}

protocコマンドを用いて、helloworld.proto を元にしたjsのファイルを自動生成するシェルです。
protocコマンドは brew install protobuf でインストールできます。また、--grpc-web_out でgrpc-web用のコードを生成しています。このオプションを利用するには事前にprotoc-gen-grpc-webをダウンロードしておく必要があります。

このシェルを実行すると、outputpathに指定したディレクトリ配下にファイルが生成されます。
スクリーンショット 2020-06-11 23.33.44.png
上記ファイルを用いた、gRPCサーバーへリクエストを投げるコードがこんな感じです。

front/src/App.tsx

import { ClientReadableStream } from "grpc-web";
import { HelloRequest, HelloReply } from "./proto/helloworld_pb";
import { GreeterClient } from "./proto/HelloworldServiceClientPb";

const sayHello = (name: string): ClientReadableStream<HelloReply> => {
  const request = new HelloRequest();
  request.setName(name);

  const client = new GreeterClient("http://localhost:8080", {}, {});
  return client.sayHello(request, {}, (err, res) => {
    if (err || !res) {
      throw err;
    }
  });
};

プロキシについて

ブラウザから直接gRPCを叩くことは現状ではできないため、envoyを使ってプロキシしています。
フロント側のコードで http://localhost:8080 宛てにリクエストを投げていますが、このリクエストをenvoyが受け取って、gRPCサーバー(http://server:50051)へ転送するようにしてます。

envoy.yaml
admin:
  access_log_path: /tmp/admin_access.log
  address:
    socket_address: { address: 0.0.0.0, port_value: 9901 }

static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address: { address: 0.0.0.0, port_value: 8080 }
    filter_chains:
    - filters:
      - name: envoy.http_connection_manager
        config:
          codec_type: auto
          stat_prefix: ingress_http
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              - match: { prefix: "/" }
                route:
                  cluster: greeter_service
                  max_grpc_timeout: 0s
              cors:
                allow_origin_string_match:
                - prefix: "*"
                allow_methods: GET, PUT, DELETE, POST, OPTIONS
                allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
                max_age: "1728000"
                expose_headers: custom-header-1,grpc-status,grpc-message
          http_filters:
          - name: envoy.grpc_web
          - name: envoy.cors
          - name: envoy.router
  clusters:
  - name: greeter_service
    connect_timeout: 0.25s
    type: logical_dns
    http2_protocol_options: {}
    lb_policy: round_robin
    hosts: [{ socket_address: { address: server, port_value: 50051 }}]

いろいろ書いてますが、ほぼ公式のチュートリアルのコピペです。変えたところは転送先のgRPCサーバーのaddressとport_valueのみです。

起動

docker-compose upでgRPCサーバーとプロキシを起動し、npm startでフロントを起動すれば、この記事の冒頭のgifを確認できます。

$ docker-compose up -d
$ cd front && npm i && npm start

ハマったところ

フロントでnpm startすると自動生成したコードの読み込みでこんなエラーが。

Failed to compile.

./src/proto/helloworld_pb.js
  Line 27:1:    'proto' is not defined     no-undef
  Line 30:15:   'proto' is not defined     no-undef
  Line 31:20:   'COMPILED' is not defined  no-undef
  Line 36:3:    'proto' is not defined     no-undef
  Line 48:1:    'proto' is not defined     no-undef
  Line 51:15:   'proto' is not defined     no-undef
  Line 52:20:   'COMPILED' is not defined  no-undef
  Line 57:3:    'proto' is not defined     no-undef
  Line 75:1:    'proto' is not defined     no-undef
  Line 76:10:   'proto' is not defined     no-undef
  Line 89:1:    'proto' is not defined     no-undef
  Line 107:1:   'proto' is not defined     no-undef
  Line 109:17:  'proto' is not defined     no-undef
  Line 110:10:  'proto' is not defined     no-undef
  Line 121:1:   'proto' is not defined     no-undef
  Line 145:1:   'proto' is not defined     no-undef
  Line 147:3:   'proto' is not defined     no-undef
  Line 159:1:   'proto' is not defined     no-undef
  Line 175:1:   'proto' is not defined     no-undef
  Line 184:1:   'proto' is not defined     no-undef
  Line 205:1:   'proto' is not defined     no-undef
  Line 206:10:  'proto' is not defined     no-undef
  Line 219:1:   'proto' is not defined     no-undef
  Line 237:1:   'proto' is not defined     no-undef
  Line 239:17:  'proto' is not defined     no-undef
  Line 240:10:  'proto' is not defined     no-undef
  Line 251:1:   'proto' is not defined     no-undef
  Line 275:1:   'proto' is not defined     no-undef
  Line 277:3:   'proto' is not defined     no-undef
  Line 289:1:   'proto' is not defined     no-undef
  Line 305:1:   'proto' is not defined     no-undef
  Line 314:1:   'proto' is not defined     no-undef
  Line 319:29:  'proto' is not defined     no-undef

Search for the keywords to learn more about each error.

issueが上がっており、そこに解決策もありました。
自動生成された helloworld_pb.js の頭に /* eslint-disable */ をつけるというパワーソリューション。

所感

gRPC-webを試しといてあれですが、ブラウザから直接gRPCを叩きたい!っていうユースケースが現状ではあまり思いつきません。こちらの記事のようにフロントとサーバーの間にBFFを挟んで、サーバー間をgRPCでやりとりするっていうのは良さそうだなと感じています。

参考

サービス間通信のための新技術「gRPC」入門
https://knowledge.sakura.ad.jp/24059/

protocの使い方
https://christina04.hatenablog.com/entry/protoc-usage

26
22
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
26
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?