grpc-web-clientをjsで試してみた

gRPC-Web: Moving past REST+JSON towards type-safe Web APIs - Improbableを見て、grpcwebを使えばgoogle.golang.org/grpc製の既存gRPCサーバがブラウザからも叩けるようになるとのことなので試してみた。

github.com

サーバ側の変更点

DOC.mdにも書いてあるように

の2点を行うだけ。

diff --git a/backend/main.go b/backend/main.go
index 0f230c4..f261ab9 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -2,8 +2,9 @@ package main
 
 import (
    "log"
-  "net"
+   "net/http"
 
+   "github.com/improbable-eng/grpc-web/go/grpcweb"
    "golang.org/x/net/context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
@@ -30,11 +31,15 @@ func main() {
    gs := grpc.NewServer(opts...)
    pb.RegisterEchoServer(gs, &server{})
 
-  l, err := net.Listen("tcp", addr)
-  if err != nil {
-      log.Fatal(err)
+   ws := grpcweb.WrapServer(gs)
+
+   mux := http.NewServeMux()
+   mux.Handle("/", http.HandlerFunc(ws.ServeHttp))
+   hs := &http.Server{
+       Addr:    addr,
+       Handler: mux,
    }
 
-  log.Println("Starting server on", l.Addr())
-  log.Println(gs.Serve(l))
+   log.Println("Starting server on", hs.Addr)
+   log.Println(hs.ListenAndServeTLS("./certs/cert.pem", "./certs/key.pem"))
 }

ブラウザ側の実装

サンプルはTypeScriptだけど、JavaScriptでも書けるみたいなのでES6でやってみた。

1. まずはprotocのプラグインts-protoc-genをインストールする
npm install --save-dev ts-protoc-gen

これで以下のように.protoからJavaScriptの定義ファイルが生成できるようになる。

protoc --plugin=protoc-gen-js_service=./node_modules/.bin/protoc-gen-js_service \
--js_out=import_style=commonjs,binary:. \
--js_service_out=. \
pb/*.proto

js_outで生成されるのが.protoのmessage
js_service_outで生成されるのが.protoのserviceになっていた。

2. 次にクライアントライブラリのgrpc-web-clientをインストールする
npm install --save google-protobuf @types/google-protobuf grpc-web-client

使い方はgrpc-web-clientのinvoke()に第1引数としてjs_service_outのrpc、第2引数としてリクエスト、接続先、各種コールバック関数をオブジェクトで渡す形になる。

import {grpc} from "grpc-web-client";

import {Echo} from "../pb/echo_pb_service.js";
import {Request} from "../pb/echo_pb.js";

function EchoCall(value) {
  const req = new Request();
  req.setValue(value);

  grpc.invoke(Echo.Call, {
    request: req,
    host: "https://localhost:9090",
    onMessage: (message) => {
      console.log("onMessage", message.toObject());
      alert(message.getValue());
    },
    onEnd: (code, msg, trailers) => {
      console.log("onEnd", code, msg, trailers);
    }
  });
}

// ちゃんと動けば引数の文字列がダイアログに出てくる
EchoCall("Hello grpc-web-client");

リクエストやレスポンスなどの各フィールドには基本的にgetterとsetterを通してアクセスすることになるみたい。

既存クライアントへの影響

気になるのは既存クライアントがどうなるのかというところ。
grpc.DialOptionの認証情報の有無で見てみると次のような結果になった。

grpc.WithInsecure()
メソッド \ サーバオプション 無し grpc.Creds()
ListenAndServe() X X
ListenAndServeTLS() X X
grpc.WithTransportCredentials()
メソッド \ サーバオプション 無し grpc.Creds()
ListenAndServe() X X
ListenAndServeTLS() O O

つまりクライアントはgrpc.WithTransportCredentials()が必須になり、grpc.Serverのオプションに関わらずListenAndServeTLS()を使えば良いということになる。
(grpc.WithInsecure()を使っていたらクライアントを直すなりサーバを分けるなりしないといけない)

まとめ

サーバ側はけっこう簡単にブラウザ対応できるし、
grpc-gatewayと比べると

が不要になるので管理するものが減って少し楽になりそう。

けど証明書が必要になるのはローカルでの動作確認とかがちょっと面倒になるかも…

まあフロントエンド的にはswaggerで生成するかprotocで生成するかの違いなので実際どっちでも良かったりするのかな?