んだ日記

ndaDayoの技術日記です

gRPC Go for Professionals 読書録 Encoding details Variants編

この前から、gRPC Go for Professionals を読んでいます。

前回の記事ではEncoding details、どのようにエンコーディングされるのか?については、すっ飛ばしました。

今のところ、すぐすぐ理解する必要はないなと判断したからです。また公式Docsにもアプリケーション作るだけであれば、Encoding detailsについては理解しなくても的な内容がありました。

が、気になる。

ここをすっ飛ばしてしまっては、面白いところを味わうことができない気がしてきました。

というわけでEncodingについて書いていきます。

protobuf.dev

今回は、こちらの公式Docsの↑のページを読んでいきます。

Encoding

公式Docsでは、Test1というスキーマを例に解説が始まります。

message Test1 {
  optional int32 a = 1;
}

このmessageがどのようにエンコードされるかですが、

When a message is encoded, each key-value pair is turned into a record consisting of the field number, a wire type and a payload.

とあります。

まずは、

  • field number
  • wire type
  • payload

の3つの用語から整理していきましょう。

field number, a wire type and a payload

message Person {
  optional string name = 1;
  optional int32 id = 2;
  optional string email = 3;
}

この例でいうと、optional string name = 1;1がfield numberです。Message Structureの中で一意で割り振ります。

次に、a wire type 。

a wire type は6種類あり、VARINT, I64, LEN, SGROUP, EGROUP, and I32がそれ。

たとえば、VARINTには、int32, int64, uint32, uint64, sint32, sint64, bool, enumがある。

optional string name = 1;の例で言うと、LENが(string, bytes, embedded messages, packed repeated fields)該当する。

最後に、payloadは、本体部分です。

公式Docsでは、a150をセットするという想定で解説していますが、150がpayloadにあたります。

Encoding payload

では、まずはpayloadのEncodingから詳細を見ていきましょう。

message Test1 {
  optional int32 a = 1;
}

このmessageのa150という値をセットしてシリアライズすると、

08 96 01

という3bytesにエンコードされます。

150が96 01にエンコーディングされます。08の部分については後ほど見ていきます。

150が96 01にエンコードされるアルゴリズム

最初に、150が96 01にエンコードされるアルゴリズムについて見ていきましょう。

まずは、150をバイナリに変換してみましょう。

150は

10010110

です。

varintのエンコーディングでは、

  • the most significant bit (MSB)を continuation bitとして扱う
  • MSBを除いた下位7bitをペイロードとして扱う

というルールで動きます。

つまり、10010110

↓msb      
1   0010110
   ↑
   payload

という分け方です。

で、下位7bitの0010110では、150のバイナリとしては不十分なので、もう1byte使って表現する必要があります。

結果は、10010110 00000001となります。

ん?

00000001  10010110

じゃないの?って思いましたよね。

これは、variantsが、little-endian orderでエンコーディングするからですね。

10010110 00000001を150に

では、次になぜ10010110 00000001が150になるのか?について、見ていきます。

考え方的にはシンプルです。

公式のDocsからそのまま掲載します。

10010110 00000001        // Original inputs.
 0010110  0000001        // Drop continuation bits.
 0000001  0010110        // Convert to big-endian.
   00000010010110        // Concatenate.
 128 + 16 + 4 + 2 = 150  // Interpret as an unsigned 64-bit integer.

まずは、continuation bits.、つまり先頭の1bitを削除します。

次に、エンコード時にlittle-endian orderで表現されていたデータをbig-endianに変換します。

くっつけると、00000010010110になるというわけですね。

ここまで来れば、150が96 01に変換されるのが腑に落ちますね。

150を、エンコーディングすると

10010110 00000001 

でした。

この2byteをそれぞれ16進数に変換すると、

10010110  →96
00000001 →01

になるというわけです。

では、続いて08 96 0108について見ていきます。

Encoding tag

再度、こちらの文章を確認しますと、

When a message is encoded, each key-value pair is turned into a record consisting of the field number, a wire type and a payload.

08 96 01のうち、96 01が150(payload)なので、08the field number, a wire typeを表現していることになります。

08の部分をtagと公式では読んでいますのでこれ以降はtagと呼びます。

formula of Encoding tag

tagのエンコードの式は、

(field_number << 3) | wire_type

です。

ビット左シフト演算子とOR演算子がございますね。

具体的に式に数字を当てはめてみましょう。

(field_number << 3) | wire_type

(1 <<< 3)  | 0

です。

(field_number << 3)

まずは、(field_number << 3)から計算してみましょう。

field_numberの1は、bitに直すと00000001です。

<< 3なので、3bit分、左にシフトしまして、結果は00001000となります。00001000です。

| wire_type

次に、wire_typeとのOR演算をしていきます。variantsのwire_typeは0です。

0とOR演算しても、元の数値は変わりませんよね。

ということで、結果は0000100008になるというわけです。

decode

では、最後にdecodeについて見てみましょう。

In other words, after decoding the varint representing a field, the low 3 bits tell us the wire type, and the rest of the integer tells us the field number.

decodeする場合には、下位3bitに対して0x07(00000111)をAND演算します。そうすると、wire_typeを導き出すことができます。

で、上位5bitがfield_numberを表現してるのでdecodeは簡単ですね。

まとめ

以上、Encoding details Variants編をみていきました。

2進数とかビット演算など、不慣れで理解に時間がかかりましたが、紐解いてみると各所はやっていることはシンプルですね。

理解すると気持ち良きですが、「じゃあ、gRPCの構築にどう役立てるんだい??」と自問するとよくわからんですね。

ひとまずは、Encoding details Variantsを理解したということで次は、その他のエンコードについてみていきたいと思います。

僕から以上。あったかくして寝ろよ。