gRPC-Web Hello World Guideをやってみた

[こちらのガイド【gRPC-Web Hello World Guide】](https://github.com/grpc/grpc-web/tree/master/net/grpc/gateway/examples/helloworld)を試してみた備忘録となります。
この記事にあるコードもほぼ参考サイトを流用させていただいております。
間違いなどありましたら指摘お願いします。

# 1. 試した環境

- macOS Mojave
- Docker version 18.09.2, build 6247962
- protoc : libprotoc 3.7.1
- protoc-gen-grpc-web-1.0.4
- node : v10.14.1
- Google Chrome : 75.0.3770.100 (Official Build) (64-bit)

【[自分で試したコードはこちら](https://github.com/okumurakengo/grpc-web-sample)】

# 2. Protocol Buffersの定義ファイルを作成

gRPCのserviceを定義。
あとでこの`.proto`ファイルから`.js`ファイルを生成します。

```helloworld.proto
syntax = "proto3";

package helloworld;

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

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}
```

# 3. gRPCサービスの実装

nodeで作成

```server.js
const PROTO_PATH = __dirname + '/helloworld.proto';

const grpc = require('grpc');
const protoLoader = require('@grpc/proto-loader');
const packageDefinition = protoLoader.loadSync(
    PROTO_PATH,
    {keepCase: true,
     longs: String,
     enums: String,
     defaults: true,
     oneofs: true
    });
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
const helloworld = protoDescriptor.helloworld;

function doSayHello(call, callback) {
  callback(null, {
    message: 'Hello! ' + call.request.name
  });
}

function getServer() {
  const server = new grpc.Server();
  server.addService(helloworld.Greeter.service, {
    sayHello: doSayHello,
  });
  return server;
}

if (require.main === module) {
  const server = getServer();
  server.bind('0.0.0.0:9090', grpc.ServerCredentials.createInsecure());
  server.start();
}

exports.getServer = getServer;
```

# 4. プロキシの設定

Envoy Proxyを使います。

```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:
                - "*"
                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
                # enabled: true #201912追記 ここの設定が削除されていました
          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
    # win/mac hosts: Use address: host.docker.internal instead of address: localhost in the line below
    hosts: [{ socket_address: { address: host.docker.internal, port_value: 9090 }}]
```

```envoy.Dockerfile
FROM envoyproxy/envoy:latest
COPY ./envoy.yaml /etc/envoy/envoy.yaml
CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml
```

# 5. クライアントのコードを作成

### 5-1. `client.js`

```client.js
const {HelloRequest, HelloReply} = require('./helloworld_pb.js');
const {GreeterClient} = require('./helloworld_grpc_web_pb.js');

const client = new GreeterClient('http://localhost:8080');

const request = new HelloRequest();
request.setName('World');

client.sayHello(request, {}, (err, response) => {
  console.log(response.getMessage());
});
```

`helloworld_pb.js`と`helloworld_grpc_web_pb.js`は`helloworld.proto`から自動生成されます。

### 5-2. `package.json`

```package.json
{
  "name": "grpc-web-simple-example",
  "version": "0.1.0",
  "description": "gRPC-Web simple example",
  "devDependencies": {
    "@grpc/proto-loader": "^0.3.0",
    "google-protobuf": "^3.6.1",
    "grpc": "^1.15.0",
    "grpc-web": "^1.0.0",
    "webpack": "^4.16.5",
    "webpack-cli": "^3.1.0"
  }
}
```

### 5-3. `index.html`

```index.html
<!DOCTYPE html>
<meta charset="UTF-8">
<title>gRPC-Web Example</title>
<script src="dist/main.js"></script>
```

# 6. `protoc`から`.js`ファイルを生成する

`.proto`ファイルの定義からjsファイルを作成するためには、`protoc`と`protoc-gen-grpc-web`が必要

### 6-1. `protoc`をインストール

こちらを参考にインストールしました
http://google.github.io/proto-lens/installing-protoc.html

macで試したのでbrewでインストールしました。

```bash:bash
$ brew install protobuf
$ which protoc #インストールされたか確認
/usr/local/bin/protoc
```

### 6-2. `protoc-gen-grpc-web`をダウンロード

こちらのリンクからmac用の`protoc-gen-grpc-web-1.0.4-darwin-x86_64`をダウンロードさせていただきました。

https://github.com/grpc/grpc-web/releases

![Screen Shot 2019-06-30 at 16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/b800de5b-d35a-9d5d-9e62-20e9c7fd3ee7.png)


ダウンロードしたらPATHの通っているフォルダに移動させる

```bash:bash
$ sudo mv ~/Downloads/protoc-gen-grpc-web-1.0.4-darwin-x86_64 \
/usr/local/bin/protoc-gen-grpc-web
chmod +x /usr/local/bin/protoc-gen-grpc-web
```

これで`protoc`と`protoc-gen-grpc-web`が使える状態になったので、以下のコマンドでjsファイルを生成する

```bash:bash
$ protoc -I=. helloworld.proto \
  --js_out=import_style=commonjs:. \
  --grpc-web_out=import_style=commonjs,mode=grpcwebtext:.
```

`helloworld.proto`から`helloworld_pb.js`と`helloworld_grpc_web_pb.js`が自動生成されたことを確認しました。

```bash:bash
$ ls -l helloworld*
-rw-r--r--  1 kengookumura  staff   1084 Jun 30 15:19 helloworld.proto
-rw-r--r--  1 kengookumura  staff   6645 Jun 30 16:45 helloworld_grpc_web_pb.js
-rw-r--r--  1 kengookumura  staff  14533 Jun 30 16:45 helloworld_pb.js
```

# 7. jsファイルをwebpackでバンドルする

```bash:bash
yarn
yarn webpack client.js --mode development
#または、
npm i
npx webpack client.js --mode development
```

`dist/main.js`が作られていればOK

# 8. サンプルを実行

ここまでで実行の準備ができましたので、実際に動かしてみます。

`server.js`実行

```bash:bash
node server.js #ポート9090でリッスンします
```

Envoyプロキシを実行

```bash:bash
docker build -t helloworld/envoy -f ./envoy.Dockerfile .
docker run -d -p 8080:8080 helloworld/envoy
```

`index.html`を表示するためのwebサーバーをなんでもいいので起動する

```bash:bash
#nodeで簡易サーバ
yarn add -D node-static
yarn static -p 8081
#phpで簡易サーバ
php -S 0.0.0.0:8081
#pythonで簡易サーバ
python3 -m http.server 8081
```

http://localhost:8081/

<img width="739" alt="Screen Shot 2019-06-30 at 18.12.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/ab29f172-83e9-f28d-e6f2-a86158523d2b.png">

`Hello! World`とコンソールに出ていたら成功しています。

---

Readmeの手順にそって行いましたが、参考にさせていただいたコードの方では、`Hello! World`の出力部分以外の処理もあり、

https://github.com/grpc/grpc-web/blob/master/net/grpc/gateway/examples/helloworld/server.js
https://github.com/grpc/grpc-web/blob/master/net/grpc/gateway/examples/helloworld/client.js

<img width="1067" alt="Screen Shot 2019-06-30 at 17.28.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/4bdd46b4-3a03-20d4-c52f-60acdf10c576.png">


`Hey! World0`〜`Hey! World4`の部分で繰り返し処理のサンプル。
`Got error, ...`の部分でエラー処理のサンプルのようですので、こちも参考にすると良いと思いました。

---

# gRPC-Webのchrome拡張も試してみた

こちらを試してみました

【[gRPC-Web Developer Tools - Chrome Web Store](https://chrome.google.com/webstore/detail/grpc-web-developer-tools/ddamlpimmiapbcopeoifjfmoabdbfbjj/related?hl=en)】

githubはこちら

【[SafetyCulture/grpc-web-devtools](https://github.com/SafetyCulture/grpc-web-devtools)】

```client.js
const {HelloRequest, HelloReply} = require('./helloworld_pb.js');
const {GreeterClient} = require('./helloworld_grpc_web_pb.js');

const enableDevTools = window.__GRPCWEB_DEVTOOLS__ || (() => {});
const client = new GreeterClient('http://localhost:8080');
enableDevTools([
    client,
]);

const request = new HelloRequest();
request.setName('World');

client.sayHello(request, {}, (err, response) => {
  console.log(response.getMessage());
});
```

```client.js
// ここの部分を追加、更新しています
const enableDevTools = window.__GRPCWEB_DEVTOOLS__ || (() => {});
const client = new GreeterClient('http://localhost:8080');
enableDevTools([
    client,
]);
```

このように、`helloworld.proto`で定義した内容に対して、値がわかりやすいように表示してくれました。

<img width="1072" alt="Screen Shot 2019-06-30 at 18.41.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/a0483a0c-d302-e973-c20e-fb9eea60b2a1.png">

---

最後まで読んでいただいてありがとうございました。