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


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

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

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

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 などしておく。

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 しておく)

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

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

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 から使うコードはこうなる。

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 いれて雑にビルド設定を書く(フロントエンドのいつものアレ)

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