この記事について
この記事では gRPC の Go言語実装である grpc-go を元に、 gRPC のメソッド呼び出しが HTTP/2 を使ってどう実現されているかをまとめる。
- 環境情報
- Go 言語のバージョン : 1.14.4
- grpc-go のバージョン : 1.29.1
はじめに
この記事では、以下のようなシンプルな protobuf の定義(*.protoファイル)を用いて確認をしている。
syntax = "proto3";
package helloworld;
service Greeter {
// Unary RPC
rpc SayHello (HelloRequest) returns (HelloResponse) {}
// Server streaming RPC
rpc SayHello_SS (HelloRequest) returns (stream HelloResponse) {}
// Client streaming RPC
rpc SayHello_CS (stream HelloRequest) returns (HelloResponse) {}
// Bidirectional streaming RPC
rpc SayHello_BI (stream HelloRequest) returns (stream HelloResponse) {}
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 1;
}
メソッド呼び出しと応答のフレーム
概要
クライアントのプログラム内で gRPC のメソッドが呼ばれると、そのメソッドの情報や引数、戻り値などが HTTP/2 フレームで送受信される。以下は Unary のメソッドを呼び出した場合の例。
- メソッドの情報
- 呼び出されたメソッドの情報(サービス名、メソッド名など)は HTTP/2 の HEADERS フレームでサーバーに送られる
- サーバーはこれによりどのメソッドが呼ばれたかを判断して必要な初期化処理などを行う
- メソッドの引数
- メソッドの引数は DATA フレームでサーバーに送られて、サーバー側に実装されたメソッドに渡される
- ステータス
- サーバー側に実装されたメソッド呼び出しの成否、失敗した場合のエラーメッセージなどを HEADERS フレームでクライアントへ返す
- 戻り値
- メソッドの戻り値を DATA フレームで返す
詳細
メソッドの情報
クライアントで呼び出したメソッドの情報は、以下のような HEADERS フレームでサーバーに送信される。
- HTTP メソッド :
POST
- パス:
/サービス名/メソッド名
(例: /helloworld.Greeter/SayHello) - その他の HTTP ヘッダー (content-type や gRPC 固有のヘッダーなど)
それぞれをもう少し詳しく見ると次のようになる。
HTTPメソッド
gRPC メソッドの種類(Unary, Server-Streaming, Client-Streaming, Bidirectional-Streaming) によらず、HTTPリクエストの POST
が使われる
パス
protobuf(*.proto ファイル)での定義に従い /サービス名/メソッド名
という形式になる。サービス名は package
と service
で指定された名前。(例) helloworld.Greeter
HTTP ヘッダー
標準的な HTTP ヘッダーだけでなく gRPC 固有のヘッダーも複数用いるが、以下によく使われるものだけ記載。
- content-type
- 必須のヘッダー
- 値は
application/grpc
、または、その後ろに+
か;
に続けてサブタイプを付けたもの- 例)
application/grpc+proto
、application/grpc+json
- サブタイプはペイロードのエンコーディング方式を表す
- 省略された場合のデフォルト値は
proto
(protobuf の意味)
- 例)
-
content-type
が無い、または値がapplication/grpc
で始まってない場合は、エラーになる
メソッドの引数
メソッドの引数は DATA フレームのペイロードに乗せてサーバーに送られる。ペイロードは gRPC の「固定長ヘッダー」と「メッセージ」から構成される。
以下にペイロードの構造を記載。
- 固定長ヘッダー(5バイト) ※青色の部分
- ペイロードを圧縮してるかどうかのフラグ (1バイト)
- 0 は未圧縮の意味
- メッセージ部分の長さ(4バイト)
- この場合、メッセージは7バイト
- ペイロードを圧縮してるかどうかのフラグ (1バイト)
- メッセージ ※オレンジ色の部分
- この場合は protobuf でエンコードされていて "0a 05 77 6f 72 6c 64" は、以下を意味する
- メッセージのフィールド番号が 1、型は Length-delimited、データ長は 5バイト、データは "world"
- 見方は、 Protocol Buffers: バイナリフォーマット(Wire Format)の中身 を参照
- この場合は protobuf でエンコードされていて "0a 05 77 6f 72 6c 64" は、以下を意味する
ステータス
サーバー側でのメソッドの呼び出し成否やエラーメッセージを HEADERS フレームで返す。
- 呼び出し自体が成功すれば、そのメソッドがエラーを返したとしても HTTP のステータスコードは 200 がクライアントに送られる
- メソッドが返したエラーメッセージ(error.Error())は
grpc-message
ヘッダーとしてクライアントに送られる - メソッド呼び出し自体が失敗した場合(例:クライアントから送られた
content-type
ヘッダーがapplication/grpc
から始まってない) 200 以外が返る。この例では 415 になる。
戻り値
戻り値は DATA フレームで引数と同様のペイロード形式でクライアントに送られる。
gRPC ストリーミングの場合
Server-Streaming, Client-Streaming, Bidirectional-Streaming のいずれかのストリーミングの場合、メソッド情報は HEADERS フレームで送られるが、gRPC のストリームに対して Send したメッセージは全て DATA フレームで送られる。
HTTP/2 ストリームとの関係
Unary の場合
1回の Unary メソッドの呼び出し/引数/戻り値は、同一の HTTP/2 ストリームで送受信される。
続けて同じメソッドを呼んだとしても別のストリームが使われる。
以下は同じ Unary のメソッドを2回続けて呼んだ例。1回目(青)は Stream ID 1、2回目(赤)は Stream ID 3 を用いていることが分かる。
ストリーミングの場合
メソッド呼び出し(HEADERS フレーム)、および、その呼び出しで取得した同一の gRPC ストリームに Send するメッセージ(DATA フレーム)は、全て同じ HTTP/2 ストリームで送受信される。
以下は Server-Streaming の例。メソッド情報送信に使われた Stream ID 1 が、サーバーからのストリーミングデータ送信にずっと使われていることが分かる。