gRPC-webがどのようにリクエストをシリアライズしているのか
事前に、gRPC-webを介して通信するためのシリアライズされたリクエストを大量に生成しておきたいお気持ちになったのでどうするか調べるというモチベーション
gRPCとは
- そもそもgRPCを見ていると2つのプロトコルがある
Due to browser limitation, the Web client library implements a different protocol than the native gRPC protocol.
ブラウザではHTTP2で通信できない(browser limitation)ので、通信できるようにしたプロトコル
ざっと抜き出した特徴
- Content-Typeを
application/grpc-web
またはapplication/grpc-web-text
とすることでテキスト形式で通信できる - HTTP2によらずHTTP/*で通信可能
gRPC-Webとは
gRPCをブラウザークライアントから使用できるようにしたJavaScriptの実装
responseのデコード方法
responseをどのようにdecodeするかは以下のコメントがわかりやすかったので訳してメモ
レスポンスをブラウザ上で確認すると↓のようになっている
AAAAAAcKBWhlbGxvgAAAAehncnBjLX....
- これはbase64でエンコードされているのでまずはデコードする
- デコード結果はバイト列になっている
- バイト列を
xxd
などを使って各バイトの値を見ると以下のようになる
echo "AAAAAAcKBWhlbGxvgAAAAehncnBjLXN0YXR1czowDQpncnBjLW1lc3NhZ2U6T0sNCnNlYy1mZXRjaC1tb2RlOmNvcnMNCngtdXNlci1hZ2VudDpncnBjLXdlYi1qYXZhc2NyaXB0LzAuMQ0KY3VzdG9tLWhlYWRlci0xOnZhbHVlMQ0KdXNlci1hZ2VudDpNb3ppbGxhLzUuMCAoWDExOyBMaW51eCB4ODZfNjQpIEFwcGxlV2ViS2l0LzUzNy4zNiAoS0hUTUwsIGxpa2UgR2Vja28pIENocm9tZS83Ny4wLjM4NjUuOTAgU2FmYXJpLzUzNy4zNg0KYWNjZXB0OmFwcGxpY2F0aW9uL2dycGMtd2ViLXRleHQNCngtZ3JwYy13ZWI6MQ0Kb3JpZ2luOmh0dHA6Ly9sb2NhbGhvc3Q6ODA4MQ0Kc2VjLWZldGNoLXNpdGU6c2FtZS1zaXRlDQpyZWZlcmVyOmh0dHA6Ly9sb2NhbGhvc3Q6ODA4MS9lY2hvdGVzdC5odG1sDQphY2NlcHQtbGFuZ3VhZ2U6ZW4tVVMsZW47cT0wLjkNCngtZm9yd2FyZGVkLXByb3RvOmh0dHANCngtcmVxdWVzdC1pZDo5OTM2YmVjMS0zZjVmLTRkZGMtYjg1OS1iZDAxZTdjOGQ1YjUNCg==" | base64 -d | xxd
00000000: 0000 0000 070a 0568 656c 6c6f 8000 0001 .......hello....
00000010: e867 7270 632d 7374 6174 7573 3a30 0d0a .grpc-status:0..
00000020: 6772 7063 2d6d 6573 7361 6765 3a4f 4b0d grpc-message:OK.
00000030: 0a73 6563 2d66 6574 6368 2d6d 6f64 653a .sec-fetch-mode:
...(以下省略)
いくつかの塊になっているが、この塊は以下のフォーマットとなっておりこの塊が繰り返して出力される
- marker
- 4バイトで示された長さ
- 示された長さバイトデータとtailer
実際に↑に合わせて解析してみる。
-
この例でいうmarkerとして
00
から始まっている。次はデータフレームだよということを示すマーカー -
次の4バイトが
00 00 00 07
となっており、データフレームの長さが7バイトであることを示す -
2で指定された7バイトを読むと
0a 05 68 65 6c 6c 6f
これがprotobufのseq=1フィールドに与えられたメッセージがエンコードされたバイト配列ちなみにこれはprotocの--decode_rawオプションを使ってデコードできる
~$ echo -n -e '\x0a\x05\x68\x65\x6c\x6c\x6f' | protoc --decode_raw 1: "hello"
-
次が
80
でこれがtrailer
フレームとなっている -
次の4バイト
00 00 01 e8
がtrailerの長さ
1e8は16進数で488なので次の488バイトがtrailerフーレムの長さとなる -
以下はバイト列の解析なので省略
responseのパーサー
protobuf objects から grpc-web-text (base64)に変換するにはencodeProtobuf
を使用すると良い
const toBytesInt32 = (num: number): Uint8Array => {
// an Int32 takes 4 bytes
const arr = new ArrayBuffer(4)
const view = new DataView(arr)
// byteOffset = 0; litteEndian = false
view.setUint32(0, num, false)
return new Uint8Array(arr)
}
const createHeader = (name: string, value: string): Buffer => {
const buffers: Array<number> = []
for (const char of name) {
buffers.push(char.charCodeAt(0))
}
buffers.push(':'.charCodeAt(0))
for (const char of value) {
buffers.push(char.charCodeAt(0))
}
buffers.push('\r'.charCodeAt(0))
buffers.push('\n'.charCodeAt(0))
return Buffer.from(buffers)
}
export const encodeProtobuf = <
GRPCReply extends { serializeBinary: () => Uint8Array },
>(
reply: GRPCReply,
): string => {
const replyBuffer = reply.serializeBinary()
const replyLength = toBytesInt32(replyBuffer.length)
const headers = Buffer.from([
...createHeader('grpc-status', '0'),
...createHeader('grpc-message', 'OK'),
])
const headersLength = toBytesInt32(headers.length)
const response = Buffer.from([
0, // mark response start ("data" frame coming next)
...replyLength,
...replyBuffer,
128, // end of "data" frame, trailer frame coming next
...headersLength,
...headers,
])
return response.toString('base64')
}
function main(): void {
// UserPB is protobuf object created with protoc-gen-grpc
// Any protobuf object with a serializeBinary function
// which returns an Uint8Array will do
const reply = new UserPB.SetUserReply()
reply.setUserId('mock-user-id')
reply.setFirstName('foo')
reply.setLastName('bar')
const encoded = encodeProtobuf(reply)
console.log(encoded)
}
main()
リクエストをUint8Arrayにしているクライアントの実装もあった
これを使ってUint8Arrayにした後にbase64でencodeすると、grpc-web-testとして使用できるリクエスト文字列が取得できる
[結論]
これを利用するのが一番良かった
k6でgrpcとしての接続ではなく、gRPC-webを介して負荷試験をしたい場合などは、事前にこの方法でリクエストデータを作成しておくことでhttp.postできる
ただし、今回の負荷試験ではレスポンスの解析については深くは必要ないのでこれ以上は追わない