gRPC over HTTP2

gRPCはRPCのフレームワークです。プロトコルはHTTP/2上に定義されています。普段gRPCを使っているもののプロトコル仕様を見たことがなかったので見てみました。

動かしたサンプルはHello Worldです。 このサンプルでgRPCサーバは"Greeting: Hello <クライアントが送った文字列>"のようにレスポンスをします。 クライアントからは"world"という文字列を送っているので実行すると以下のようになります。

クライアント

$ go run greeter_client/main.go
2019/12/13 20:49:26 Greeting: Hello world

サーバ

$ go run greeter_server/main.go
2019/12/13 20:49:26 Received: world

これをWiresharkでキャプチャしてgRPCリクエスト部分だけ抜き出すと以下のようになっています。

f:id:gkuga:20191213155843p:plain

HTTP/1.1ではHTTPリクエストとHTTPレスポンスがメッセージの最小の単位でした。HTTP/2ではHTTPフレームが最小の単位となっています。今回では HEADERSフレーム + DATAフレームが1つのgRPCリクエストとなっています。

gRPCの定義は↓となっています。

Request → Request-Headers *Length-Prefixed-Message EOS

ABNF syntaxで記述されています。gRPCリクエストはRequest-Headersと0個以上のLength-Prefixed-MessageEOSフラグが付いたLength-Prefixed-Messageから構成されます。

Request-HeadersがHTTP/2のHEADERSフレームで、Length-Prefixed-MessageDATAフレームに対応します。EOSはEOSフラグが付いたDATAフレームです。

このDATAフレームにgRPCリクエストのメッセージが入っているようです。今回のサンプルでは下記のようにメッセージが定義されています。

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

HTTP/2 DATA Frame

DATAフレームを見ると↓のようになっています。(HEADERSフレームの説明は省略します)

f:id:gkuga:20191213175026p:plain

HTTP/2のフレームは以下のように定義されています。(rfc7540より引用)

f:id:gkuga:20191213164810p:plain

16進数で表されたDATAフレームを順に見ていきます。最初の3バイトはペイロードの長さを表しています。

00 00 0c

12バイト(0x0c)がフレームペイロードの長さで、Length-Prefixed-Messageが12バイトあるということです。次の1バイトはHTTP/2のフレームのタイプで、00はDATAフレームを表しています。

00

次の1バイトはフラグで、今回は↓のようにEND_STREAM (0x1)フラグが付いています。

01

このフラグは名前の通りHTTP/2のストリームの状態をhalf closedにさせます。続いて、Rは常に0x0である予約された1ビットです。その次のストリームIDが31ビットで合わせた4バイトは以下のようになっています。

00 00 00 01

ストリームIDは1です。

gRPC Message in DATA Frames

続いてフレームペイロード部分です。Wiresharkの表示がDATAフレームのあとにgRPC Messageが送られているようにも見えますが、DATAフレームの中にgRPC Messageがあります。

f:id:gkuga:20191213182546p:plain

このDATAフレームはgRPCリクエストの定義のLength-Prefixed-Messageの部分ですが、さらに以下のように定義されています。

Length-Prefixed-Message → Compressed-Flag Message-Length Message

このLength-Prefixed-Messageは12バイトでした。最初の1バイトはCompressed-Flagです。

00

これが1だとHEADERSフレーム中にあるMessage-Encoding headerで指定された形式でMessageが圧縮されています。今回は圧縮されていないようです。次の4バイトはMessageの長さです。

00 00 00 07

長さは7バイトです。そして最後が7バイトのMessageです。

0a 05 77 6f 72 6c 64

これはProtocol Buffersで、今回は既に掲載しましたが、

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

というgRPCリクエストメッセージの定義で、このnameに"world"という文字列入れて送っています。Protocol BuffersではKey-Value形式にエンコーディングしています。この場合のKeyはstring name = 11です。nameという情報はMessageには含まれていません。(なのでプログラムで使う時はProtocol Buffersの定義ファイルをサーバとクライアントで共有することになります。) Valueworldという文字列です。エンコーディングされたMessageを説明するために2進数にします。

00001010 00000101 01110111 01101111 01110010 01101100 01100100

まずKeyの部分ですが、最初の1バイトである00001010です。このmsb(最上位ビット)が1だと次の1バイトもKeyを表しますが、今回はmsbは0なので、最初の1バイトがKeyの部分となります。Keyの下位3ビットはValueのタイプを表していて今回は010です。これはProtocol BuffersではLength-delimitedという文字列などで使われるタイプです。msbと下位3ビットを除いた0001がKeyで1です。

Length-delimitedValue部分はValueの長さとValueから構成されます。2バイト目の00000101が長さです。msbが次の1バイトにも続くかどうかのフラグになっているのは同様です。今回も0なので0000101が長さを表しています。長さは5です。"world"で5バイトですね。stringはUTF8として定義 されています。

01110111 01101111 01110010 01101100 01100100
(77 6f 72 6c 64)

UTF8の英数字はASCIIなので、確認するとworldとなっています。

gRPCレスポンス

レスポンスの定義は次のようになっています。

Response → (Response-Headers *Length-Prefixed-Message Trailers) / Trailers-Only

キャプチャしたgRPCレスポンスを見ると次のようになっています。

f:id:gkuga:20191213215903p:plain

↑がResponse-Headers *Length-Prefixed-Messageで↓がTrailersです。

f:id:gkuga:20191213192219p:plain

Response-HeadersがHTTP/2のHEADERSフレームで、Length-Prefixed-MessageDATAフレームに対応します。そして、TrailersはEND_STREAMフラグが付いたHEADERSフレームです。DATAフレームにはリクエストの時と同様にgRPCレスポンスメッセージがあります。詳細に追うのは省略します。

Trailersでストリームがclosedになります。(rfc7540より引用)

f:id:gkuga:20191213225157p:plain

試しにserverからgRPCレスポンスでメッセージを返さずにエラーを返すと、Trailers-Onlyになりました。

// return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
return nil, status.Error(codes.Internal, "something wrong")

f:id:gkuga:20191213235846p:plain

最後に

Server streaming RPCなども追ってみたいです。またいずれ書きます。

HTTP/2の上のレイヤーとして定義されているので、HTTP-Statusを"200"に固定していたり、HTTPとしては意味をなしていない部分もあるようです。メリットの方が大きいということでしょうか。ちょっとググったらgRPC Design and Implementationを見つけました。

Leverage existing network protocols and infrastructure

Functional in production data center, but not on Internet (firewalls, etc)

Programmer responsible for all network management and data transfer

と書いています。Network managementはgRPCサーバがうまいことやってくれればいいかなとは思います。GoogleではSaaS事業者が提供しているAPIがgRPCみたいな感じにgRPCが使われている感じでしょうか。gRPCについて議論されているドキュメントや書籍あれば教えてください。

アウトプットの習慣が今までなかったり効率が悪くて記事を書くのにすごい時間かかる...。

参考