grpc-web-clientをjsで試してみた
gRPC-Web: Moving past REST+JSON towards type-safe Web APIs - Improbableを見て、grpcwebを使えばgoogle.golang.org/grpc製の既存gRPCサーバがブラウザからも叩けるようになるとのことなので試してみた。
サーバ側の変更点
DOC.mdにも書いてあるように
- grpc.Serverをgrpcweb.WrappedGrpcServerに変換して
- http.ServerからServeするようにする
の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と比べると
- proxyサーバ
- .protoのREST定義
が不要になるので管理するものが減って少し楽になりそう。
けど証明書が必要になるのはローカルでの動作確認とかがちょっと面倒になるかも…
まあフロントエンド的にはswaggerで生成するかprotocで生成するかの違いなので実際どっちでも良かったりするのかな?