node で grpc を使ってみる + TypeScript でブラウザ実行しようとした

grpcを手元で動かすのにシンプルな例を探していたが、全然見つからなかったのでメモがてら書いておく。

(Googleの公式サンプル、非常にわかりづらすぎる… https://grpc.io/docs/tutorials/basic/node.html)


まず通信定義を書く。 service の rpc を定義する。

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

package helloworld;

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

message HelloRequest {
  string name = 1;
}

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

そのサーバーを実装する。事前に `npm install -S grpc` などしておく。

```js:server.js
const PROTO_PATH = __dirname + '/helloworld.proto'
const grpc = require('grpc')
const { helloworld } = grpc.load(PROTO_PATH)

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

const server = new grpc.Server()
server.addService(helloworld.Greeter.service, { sayHello })
server.bind('0.0.0.0:50051', grpc.ServerCredentials.createInsecure())
server.start()
```

`node server.js` で localhost:50051 にサーバーが立つ。

これを呼び出すクライアントを書く。今回は `grpc-caller` を使う。(npm install しておく)

```js:client.js
const path = require('path')
const caller = require('grpc-caller')

const PROTO_PATH = __dirname + '/helloworld.proto'
const client = caller('0.0.0.0:50051', PROTO_PATH, 'Greeter')

client.sayHello({ name: 'Bob' }, (err, res) => {
  console.log(res)
})
```

grpcサーバーを立てた状態で `node client.js` で実行すると `{ message: 'Hello Bob' }` と出るはず。

ここまでは簡単

## ブラウザで実行する

ここから先、調べたが typescript の例しかなかったので、 typescript を使う。バニラJS のみでシンプルに確認作業を終えたかったが、確かにgrpcの目的としては型がない環境を想定しないので、ここは諦める。

`grpc` は native module なので、ブラウザで呼び出すために `grpc-web-client` を使う。

ついでに、helloworld.proto からクライアントコードも生成する必要がある。(この辺 github のリポジトリ名が grpc-web だったりして別パッケージ化と思ったが同一のようだった)

ってことで、 protoc コマンドをインストールして、ついでにその typescript plugin をいれる。

```
brew install protobuf # Mac
npm install -S ts-protoc-gen
```

こんなシェルスクリプト書く

```sh:gen.sh
# Path to this plugin
PROTOC_GEN_TS_PATH="./node_modules/.bin/protoc-gen-ts"

# Directory to write generated code to (.js and .d.ts files)
OUT_DIR="./out"

protoc \
    --plugin="protoc-gen-ts=${PROTOC_GEN_TS_PATH}" \
    --js_out="import_style=commonjs,binary:${OUT_DIR}" \
    --ts_out="service=true:${OUT_DIR}" \
    helloworld.proto

```

`sh gen.sh` で実行すると out にこんなコードが出力される

```
⋊> ~/s/protobuf-playground on master  tree out                                                                                   23:44:20
out
├── helloworld_pb.d.ts
├── helloworld_pb.js
├── helloworld_pb_service.d.ts
└── helloworld_pb_service.js
```

これを Typescript から使うコードはこうなる。

```ts
import { grpc } from 'grpc-web-client'
import { HelloRequest } from './out/helloworld_pb'
import { Greeter, GreeterClient } from './out/helloworld_pb_service'
const HOST = 'http://localhost:50051'

const req = new HelloRequest()
req.setName('johndoe')

const client = new GreeterClient('http://localhost:50051')
client.sayHello(req, (err, ret) => {
  if (err) {
    throw err
  }
  console.log(ret)
})
```

`yarn tsc --init` でtsconfig.jsonを生成

webpack と ts-loader いれて雑にビルド設定を書く(フロントエンドのいつものアレ)

```js:webpack.config.json
module.exports = {
  mode: 'development',
  entry: './web-client.ts',
  output: {
    path: __dirname + '/web',
    filename: 'bundle.js'
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js']
  },
  module: {
    rules: [{ test: /\.tsx?$/, loader: 'ts-loader' }]
  }
}
```

で、ここで自分はこういうエラーがでたが問題はなかった(tsconfigの設定が悪そう。moduleResolution とかあのへん。今回は本題ではないので追跡しない)

```
ERROR in /Users/mz/sandbox/protobuf-playground/web-client.ts
./web-client.ts
[tsl] ERROR in /Users/mz/sandbox/protobuf-playground/web-client.ts(1,22)
      TS2307: Cannot find module 'grpc-web-client'.
```

で, 適当に index.html 置いてこの js を読み込むと動くはず…

ここで謝らないといけないことがある。これでおそらく実行できているのだが、localhostでhttpのみの環境でやっていたので、実際に grpc の疎通を確認できなかった。(gprcはhttp/2必須)

こんなエラーが出て頓挫した。

```
fetch.js:42 OPTIONS http://localhost:50051/helloworld.Greeter/SayHello 0 ()
Fetch.send @ fetch.js:42
Fetch.sendMessage @ fetch.js:70
GrpcClient.send @ client.js:230
unary @ unary.js:35
sayHello @ helloworld_pb_service.js:33
(anonymous) @ web-client.ts:12
./web-client.ts @ bundle.js:811
__webpack_require__ @ bundle.js:20
(anonymous) @ bundle.js:69
(anonymous) @ bundle.js:72
detach.js:22 Uncaught Error: Response closed without headers
    at Object.onEnd (helloworld_pb_service.js:41)
    at eval (unary.js:26)
    at Array.eval (client.js:182)
    at runCallbacks (detach.js:10)
    at eval (detach.js:34)
```

たぶん http/2 だったら動いてる感じのエラーなので、あとで確認して追記する。h2o とオレオレ証明書とかそういう作業になると思う




## 感想

動作確認だけだったらとりあえず最初の grpc-caller の例でよくて、 gprc-server の開発の初手としてはややこしいブラウザは後回しで良さそう。結局やるのはそうなんだけど…

あと、どこ探しても Golang + TypeScript の例しか見つからなかった。いろんな例を眺めてコードちょっとずつ書きながら、いろんな情報の継ぎ接ぎで書いたので参照元とか思い出せない…

ここまでのコードはこれ https://github.com/mizchi-sandbox/protobuf-playground