このドキュメントは、プロトコルバッファメッセージのバイナリの搬送フォーマットについて記述しています。 アプリケーションでプロトコルバッファを使うのにこれを理解する必要はないですが、プロトコルバッファの異なった形式がエンコードメッセージのサイズにどのように効果を及ぼすのかを知ることは有用です。
message Test1 {
required int32 a = 1;
}
08 96 01
とりあえずバイト数のかなり小さな数値列になっているようです。でもこれは何を意味しているのでしょうか? それはこの後を読んでください。
上の簡単なプロトコルバッファエンコーディングを理解するためには、まず始めに *可変長整数* を理解する必要があります。 可変長整数は、整数を 1バイト以上に直列化する手法です。小さな数値は少ないバイト数を使います。
最後のバイトを除いた可変長整数のそれぞれのバイトには、 *最上位ビット* (msb) がセットされます。 これは、後に続くバイトがあることを示します。 下位の 7ビットの集まりは、 *最下位のグループを最初にした* 2の補数表現を格納するのに使われます。
0000 0001これは単一のバイトですので、msb はセットされていません。
1000 0010 0000 0001
1000 0010 0000 0001 → 000 0010 000 0001
000 0010 000 0001
→ 000 0001 ++ 000 0010
→ 10000010
→ 128 + 2 = 130
知っているように、プロトコルバッファのメッセージはキーと値のペアの連なりです。 メッセージのバイナリバージョンはフィールドの番号をキーとして使います。 フィールドの名前と宣言された型は、デコーディング側でメッセージ型の定義(.protoファイル)を参照して決定できるだけです。
メッセージがエンコードされる時に、キーと値は結合されてバイトストリームに入れられます。 メッセージがデコードされる時には、パーザは認識できないフィールドをスキップできる必要があります。 このやり方は、新しいフィールドを知らない古いプログラムを壊さずに、新しいフィールドを追加できます。 最終的に、搬送形式メッセージの個々のペアの「キー」は実際には二つの値になります。 .protoファイルにあるフィールド番号と、後に続く値の長さを見つけるのに十分な情報を提供する *搬送型* です。
| 型 | 意味 | 使い道 |
|---|---|---|
| 0 | 可変長整数 | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
| 1 | 64ビット値 | fixed64, sfixed64, double |
| 2 | 長さ指定 | string, bytes, embedded messages |
| 3 | グループの開始 | group(廃止予定) |
| 4 | グループの終了 | group(廃止予定) |
| 5 | 32ビット値 | fixed32, sfixed32, float |
(フィールド番号 << 3) | 搬送型の値になっています。つまり、数値の最後の 3ビットは搬送型を格納しています。
000 1000です。
96 01 = 1001 0110 0000 0001
→ 000 0001 ++ 001 0110 (msbを省いて、7ビットグループを逆順にする)
→ 10010110
→ 2 + 4 + 16 + 128 = 150
前のセクションで見たように、搬送型の 0 に関連付けられる全てのプロトコルバッファの型は可変長整数でエンコードされます。 ですが、負の数をエンコードする時には符号付き整数型(sint32 と sint64)と、「標準的な」整数型(int32とint64)の間に大きな違いがあります。 負の数に int32かint64 を使っている場合、結果の可変長整数は *常に10バイトの長さ* になります。 実際には、これはとても大きな正の整数のように扱われます。 符号付き型のどれかを使った場合、結果の可変長整数はより効率的な ZigZagエンコーディングが使われます。
| 元の符号付き整数 | エンコードに使われる値 |
|---|---|
| 0 | 0 |
| -1 | 1 |
| 1 | 2 |
| -2 | 3 |
| 2147483647 | 4294967294 |
| -2147483648 | 4294967295 |
(n << 1) ^ (n >> 31)でエンコードされ、64ビットでは
(n << 1) ^ (n >> 63)となります。
二番目のシフト((n << 32) 部分)は算術シフトです。 ですので、シフトの結果は全てのビットがゼロ(nが正の数の場合)にも、1にも(nが負の数の場合)なります。
sint32 や sint64 がパーズされるときに、その値は元の符号付き整数に戻されます。
可変長整数でない数値型は簡単です。 double と finxed64 の搬送型は 1 で、これはパーザは 64ビット固定長のデータ塊を予期するよう伝えます。 同様に、float と fixed32 の搬送型は 5 で、32ビットを予期するよう伝えます。 どちらの場合でも、値はリトルエンディアンで格納されます。
message Test2 {
required string b = 2;
}
12 07 74 65 73 74 69 6e 67
後半の 7バイトは "testing" の UTF8 表現です。 キーは 0x12 で、tag=2, type=2 になります。 長さを表す可変長整数の値は 7 です。 後にちょうど 7バイト続いています ― 今使っている文字列です。
message Test3 {
required Test1 c = 3;
}
1a 03 08 96 01
見て分かるように、最後の 3バイトは最初の例と全く同じです(08 96 01)。 さらに、この 3バイトの前には数値の 3があります ― 組み込みメッセージは文字列(搬送型 = 2)と全く同じ方法で扱われています。 (訳注: キーは 0x1a = 00011010 で、tag=00011=3, type=010=2 です)
メッセージ定義に repeated があったら、エンコードメッセージは、同一のタグ番号でゼロ以上のキー・値ペアを持ちます。 これらの repeted値は連続的に表れる必要はありません。別のフィールドに差し込んでも構いません。 解析時にお互いの要素の順番は保持されますが、他のフィールドの順番は失われます。
メッセージに optional な要素があると、エンコードメッセージにそのタグ番号のキー・値ペアがあるかもしれないし無いかもしれません。
MyMessage message; message.ParseFromString(str1 + str2);は、次と同じになります:
MyMessage message, message2; message.ParseFromString(str1); message2.ParseFromString(str2); message.MergeFrom(message2);
.protoファイルではフィールドの番号はどんな順番にもできます。 ですがメッセージを直列化する時には、提供されている C++, Java, Python 直列化コードのように、既知のフィールドはフィールド番号順に書き出されるべきです。 このことは、パーザコードがフィールド番号が順番に並んでいることに依存した最適化できるようにします。 しかし全てのメッセージが単純にオブジェクトを直列化して作られるわけではないので、 プロトコルバッファのパーザはどんな順序のフィールドも解析できなければなりません。 例えば、単に連結することで 2つのメッセージをマージすることが有用な時があります。
メッセージに 未知のフィールド がある場合、現在の Java と C++ の実装は、既知のフィールドを順序付けて書きだした後に、それらを任意の順番で書き出します。 現在の Python の実装は、未知のフィールドを追跡しません。