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

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

- フロント
gRPC-web、TypeScript

- サーバー
gRPC、rust、docker

- プロキシ
envoy、docker

![grpc.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131625/69b9c62c-27e9-557a-b7b2-6b957e376daa.gif)
コードは[こちら](https://github.com/pokotyan/grpc-web-rust)にあります。
こんな感じのディレクトリ構成になっており、フロント以外は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を返すというインタフェースを定義しています。

```proto: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](https://github.com/protocolbuffers/protobuf)というツールを使うことで上記のprotoファイルを元に、各言語でのインタフェースのコードを自動生成することができます。

## 3. 実装を書く
2で作成したインタフェースにしたがって実装を書いていきます。
この `インタフェースにしたがって` という部分は言語によって実現の仕方が異なっており、
- pythonであれば自動生成されたコードを継承する形でクラスを実装する
- nodeであれば、自動生成されたコードに実装の関数を引数として渡す

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

# rustでのgRPCについて
今回、rustでgRPCサーバーを作るにあたって、[tonic](https://github.com/hyperium/tonic)を利用しました。

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

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

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

```rust: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131625/a75753ef-3dd2-8ca2-3a7a-3f71f8748bfb.png)


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

```rust: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!` と返すような実装にしました。

```rust: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](https://github.com/fullstorydev/grpcurl)を使えばちゃんとレスポンスが返ってくることが確認できます。
<img width="918" alt="スクリーンショット 2020-06-11 22.00.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131625/d74defdb-1dd9-63e1-7a82-662c109c8b63.png">

# gRPC-webについて

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

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


gRPC-webの実装は、[公式のhellowWorldのチュートリアル](https://github.com/grpc/grpc-web/tree/master/net/grpc/gateway/examples/helloworld)を参考に実装しました。
gRPC-webもサーバーのときと同様に、protoファイルを元にインタフェースのコードを自動生成し、それを活用します。

```bash: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](https://github.com/pokotyan/grpc-web-rust/blob/master/server/proto/helloworld.proto) を元にしたjsのファイルを自動生成するシェルです。
protocコマンドは `brew install protobuf` でインストールできます。また、`--grpc-web_out` でgrpc-web用のコードを生成しています。このオプションを利用するには事前に[protoc-gen-grpc-web](https://github.com/grpc/grpc-web/releases)をダウンロードしておく必要があります。

このシェルを実行すると、outputpathに指定したディレクトリ配下にファイルが生成されます。
![スクリーンショット 2020-06-11 23.33.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131625/6d6bc623-7645-fbff-435a-b4e58ed058e8.png)
上記ファイルを用いた、gRPCサーバーへリクエストを投げるコードがこんな感じです。

```ts: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](https://github.com/envoyproxy/envoy)を使ってプロキシしています。
フロント側のコードで `http://localhost:8080` 宛てにリクエストを投げていますが、このリクエストをenvoyが受け取って、gRPCサーバー（`http://server:50051`）へ転送するようにしてます。

```yml: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 }}]

```

いろいろ書いてますが、ほぼ[公式のチュートリアル](https://github.com/grpc/grpc-web/tree/master/net/grpc/gateway/examples/helloworld#configure-the-proxy)のコピペです。変えたところは転送先の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](https://github.com/grpc/grpc-web/issues/447#issuecomment-459122012)が上がっており、そこに解決策もありました。
自動生成された helloworld_pb.js の頭に `/* eslint-disable */` をつけるというパワーソリューション。

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

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

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