@vvakame さんが TechBooster の新刊"JavaScriptoon"の中でgRPCを解説していて、その中で grpc-gateway にも触れている。これはとてもよい記事だったので、みんなこの本の電子書籍版を買えば良いと思う。 ただし、grpc-gatewayは記事中で使われているだけで主題ではないので、すべてのトピックをカバーしてくれているわけではない。それは仕方が無いが、そろそろgrpc-gatewayの機能を見渡す日本語記事が欲しいと思ったので自分で書くことにする。
grpc-gatewayとは
gRPC (HTTP/2 + ProtocolBuffers)をwrapして古典的なJSON API (HTTP 1.1 + JSON)を提供するリバースプロキシを生成するコード生成機だ。 別記事 にも書いた。
何ができるのか
gRPCで使うサービス定義(IDLみたいなやつ)を元にリバースプロキシの中核部分を実装するgolangのHTTP handlerライブラリを生成する。 リバースプロキシはgolangで書かれているが、これはwrapされるgRPCサービスとは別プロセスで動作するためサービス自体をgolangで書く必要はない。C++, Java, Ruby, Python, PHP, C#, Node.jsなどgRPCがサポートする任意の言語で書いて良い。
では、生成されたレバースプロキシは何ができるのか。 基本的にはHTTP/1.1 リクエストをサービス定義ファイルに埋め込まれた情報に基づいて解釈し、gRPC用のリクエストメッセージを構築したうえでgRPCエンドポイントに転送、 その後はエンドポイントからレスポンスを受け取ってからJSONに変換し直してクライアントに転送する。
Request parameter
もうちょっと詳しく見てみよう。まず、RPCのパラメータは次の3種類の形で受け付けて、3つを任意に組み合わせることもできる。
Body parameter
プロキシが受けたHTTP request bodyをProtocol Buffers messageのJSON表現として解釈し、そのmessageをgRPCエンドポイントに転送する gRPCに引き渡すmessageの一部分だけをrequest bodyから受け取ることもできる。その場合は、他の部分はpath parameterやquery parameterとして受け取る必要がある。
勿論、HTTP methodがGETやDELETEなどの場合はbody parameterは受け取れないので、path parameterやquery parameterで受け取る必要がある。
Path parameter
プロキシが受けるHTTP request pathパターンの中に変数部分を埋め込んでおくことができる。この変数部分の値を対応するmessageのフィールドに反映してからgRPCエンドポイントに転送する
Query parameter
同じく、query stringをmessageのフィールドに反映する
これらのマッピングの詳細は、Googleが公開している google.api.http というカスタムオプションを用いてサービス定義ファイルの中に指定する。下記はその例である。
service EchoService { rpc Echo(SimpleMessage) returns (SimpleMessage) { option (google.api.http) = { post: "/v1/example/echo/{id}" }; } rpc EchoBody(SimpleMessage) returns (SimpleMessage) { option (google.api.http) = { post: "/v1/example/echo_body" body: "*" }; } }
次のような項目が設定できる。
HTTP request pathのパターン(+ HTTP method)とgRPCメソッドの対応関係
- 例では全部POSTで受けているが、任意のHTTPメソッドを利用できる
Protocol Buffers messageのどの部分を(または全部を)bodyとして受け取るのか
- 例の
EchoBody
ではSimpleMessage
内の全fieldをbodyで受けている
- 例の
Path pattern内の可変部分とmessage fieldの対応
- 例の
Echo
ではパスの最後にある可変部分をSimpleMessage
のid
フィールドに対応させている
- 例の
Bodyやpathで受け付けなかったパラメータはすべて省略可能のquery parameterであると仮定される
Header
gRPCはリクエストを受け取る際にリクエストメッセージ本体の他にmetadataという名前=値ペアの列を受け取ることができる。 これをHTTP/1.1で提供するため、以下のようにHTTP headerをmetadataに変換する
- Authorizationヘッダ: そのままAuthorization metadataとして転送する
- Grpc-metadata-(varname)ヘッダ: (varname) metadataとして転送する
ところで今 RFC 2617 を見ていたらWWW-Authenticateヘッダを完全に忘れていたことに気がついたので、あとでサポートした方が良いかもしれない。
Streaming
gRPCにはstreamingという機能がある。1つリクエストを受け取って1つレスポンスを返す代わりに、リクエストのストリームやレスポンスのストリームを受け付けるものだ。 更に言うと次の3パターンがある。
- client streaming: 徐々に、あるいは散発的に送られてくるリクエストメッセージの列が完了してからレスポンスを1つ返す
- server streaming: リクエストを1つ受け取ってから徐々に、あるいは散発的にレスポンスの列を返す
- bidirectional streaming: リクエストとレスポンスを任意のタイミングで任意回送り合う
grpc-gatewayは最初の2つをサポートし、メッセージの列はJSON Streamとして表現される。 bidirectionalなstreamingも限定的にサポートしているが、すべてのリクエストを受け取ってからレスポンスを返し始めるという使い方しかできない。本当にbidirectionalにしようと思ったらWebSocketsにugpradeとかしなければならないだろう。現在はそこまでする予定はない。
なお、client streamingとbidirectional streamingではすべてのリクエストパラメータをbodyで渡す必要がある。リクエストメッセージが複数個である以上、特定のリクエストメッセージに紐付かない形のパラメータは解釈が曖昧になるからだ。
エラー処理
gRPCサービスがエラーを返した場合はエラーコードを適切なHTTP response statusに対応させて返す。またエラー時のresponse bodyにはgRPCサービスが返したエラーメッセージを含むJSON objectが入っている。