Protobufコーディングガイド

6663 ワード

このドキュメントではprotocol bufferのバイナリ有線フォーマット(binary wire format)について説明します.これらを理解してアプリケーションでprotocol bufferを使用する必要はありませんが、異なるprotocol bufferフォーマットが符号化されたメッセージ体の体積にどのように影響するかを知りたい場合は、これらの知識が役立ちます.

簡単なメッセージ


非常に簡単なメッセージ定義があるとします.
message Test1 {
  optional int32 a = 1;
}

アプリケーションでは、Test1メッセージを作成し、aを150に設定します.メッセージを出力ストリームにシーケンス化し、エンコードされたメッセージを表示できる場合は、3バイトが表示されます.
08 96 01

これまで、こんなに小さくて数字だったのに、これはどういう意味ですか.下を見続ける

Varintコード


上記protocol buffer符号化のデータを理解するには、まずvaintsVarints1バイト以上で整数を符号化する方法であることを理解する必要があります.小さい数字は少ないバイトを使用します.
最後のバイトを除いて、varint符号化の各バイトには最高有効ビット(most significant bit-msb)が設定されています.msbが1の場合、後のバイトが現在のデータに属しているかを示します.0の場合、これは現在のデータの最後のバイトデータです.各バイトの下位7ビットは、7ビットを1組の記憶数字とするバイナリ符号化表現に用いられ、最低有効グループが前であるか、または最低有効バイトが前である.これはvarint符号化後のデータのバイトが小端順に配列されていることを示している.
たとえば、数字1については、1バイト分を占有するので、バイトの最上位は0です.
0000 0001

数字300には少し複雑で、2バイトかかります.
1010 1100 0000 0010

では、どうやって300と計算しますか?まず、各バイトのmsbを削除する必要があります.それは、数字の最後のバイトに達したかどうかを示すためにのみ使用されます(この例のvarintは2バイトを占有しているので、最初のバイトのmsbは1です).
 1010 1100 0000 0010
→ 010 1100  000 0010

2つのグループの7ビットを反転します.varintが格納している数字の最低有効グループが前にあることを覚えています.次に、それらを接続して最終値を取得します.
000 0010  010 1100 ( , 7 )
→  000 0010 ++ 010 1100
→  100101100
→  256 + 32 + 8 + 4 = 300

注:varint符号化は理解しにくいので、前に書いたvarint符号化原理解析を見ることができます.

メッセージの構成


ご存知のように、protocol bufferは一連のキー値ペアです.メッセージのバイナリフォーマットは、メッセージフィールドのフィールド番号のみをキーとして使用します.フィールド名と宣言のタイプは、解析側で参照メッセージタイプ定義(すなわち.protoファイル)を参照することによってのみ決定できます.
メッセージが符号化されると、キーと値がバイトストリームに接続されます.メッセージが復号されると、アナライザは、認識されていないフィールドをスキップすることができる必要がある.これにより、新しいメッセージのフィールドは、彼らが存在することを知らない古いプログラムを破壊することはありません.このため、有線フォーマットメッセージの各ペアの「キー」は、実際には2つの値である-.protoファイルのフィールド番号に、後続の値の長さを検索するのに十分な情報しか提供しない有線タイプが追加されています.ほとんどの言語実装では、このキーをラベルと呼びます.
使用可能な有線タイプは次のとおりです.
Type
Meaning
Used For
0
Varint
int32, int64, uint32, uint64, sint32, sint64, bool, enum
1
64-bit
fixed64, sfixed64, double
2
Length-delimited
string, bytes, embedded messages, packed repeated fields
3
Start group
groups (deprecated)
4
End group
groups (deprecated)
5
32-bit
fixed32, sfixed32, float
メッセージ・ストリーム内の各キーはvarintであり、(filed_number << 3) | wire_type取得、すなわちバイトの下位3ビットが有線タイプで格納される.
次に、上記のメッセージの例に戻ります.バイトストリームの最初のバイトは常にvarintキーであり、私たちの例では08または次のバイナリ(msbを削除)であることがわかります.
000 1000

後ろの3桁から有線タイプ(0)が得られ、右に3桁移動してフィールド番号(1)が得られる.フィールドの番号が1に対応する値がvarintであることを知っています.前に学んだ復号varintの知識を使用すると、次の2バイトが150の値を格納していることがわかります.
96 01 = 1001 0110  0000 0001
       → 000 0001  ++  001 0110 ( , 7 )
       → 10010110
       → 128 + 16 + 4 + 2 = 150

その他の値のタイプ


符号付き整数


前のセクションで見たように、protocol bufferの有線タイプ0に関連付けられたすべてのタイプはvarintとして符号化されます.しかし、負数を符号化する場合、符号付きintタイプ(sint 32およびsint 64)と「標準」intタイプ(int 32およびint 64)との間には大きな違いがある.int 32またはint 64を負数のタイプとして使用すると、結果varintは常に10バイト長–実際には非常に大きな符号なし整数と見なされます.符号付きタイプ(sint 32およびsint 64)のいずれかを使用する場合、生成されたvarintはZigZag符号化を使用し、より効率的である.
ZigZag符号化は、絶対値が小さい数字(例えば−1)もvarint符号化値が小さいように、符号数が符号数なしにマッピングされる.このようにして、正の整数と負の整数とを「ジグザグ」し、-1を1、1を2、-2を3として符号化することにより、以下の表に示す.
Signed Original
Encoded As
0
0
-1
1
1
2
-2
3
2147483647
4294967294
-2147483648
4294967295

非varint数字


非varint符号化の数字と比較して簡単である--doubleおよびfixed64有線タイプ1を使用すると、解析器が固定したい64-bitのデータブロックが示される.同様にfloatおよびfixed32有線タイプ5を使用すると、解析器が固定する32ビットデータブロックを所望することを示す.どちらの場合も、バイトを小エンドシーケンスで並べてデータを格納します.

文字列


有線タイプ2(長さ区切り)は、この値がvarint符号化の長さ値であり、その後、長さ値に指定された数のデータバイトであることを示す.
message Test2 {
  optional string b = 2;
}

bの値を「testing」に設定すると、メッセージに対応するバイナリ有線フォーマットは
12 07 74 65 73 74 69 6e 67
赤いバイトはUTF-8符号化後の「testing」
ここでのキーは0 x 12→0001 0010→フィールド番号=2、タイプ=2(1バイト目の後3ビットは有線タイプの番号を表し、次に右に3ビット移動して000 0010フィールド番号を得る).値のvarintは、データのバイト長が7であることを示します.後に見つかった7バイトは、解析器が探している文字列です.

埋め込みメッセージ


以下に、埋め込みメッセージを持つメッセージ定義を示すTest3埋め込みメッセージタイプは、上記の例で定義したTest1
message Test3 {
  optional Test1 c = 3;
}

以下は内蔵Test 1 a 150,Test 3`が符号化されたバージョン
1a 03 08 96 01
ご覧のように、最後の3バイトは、私たちの最初の例で符号化された結果と同じです(08 96 01)、彼らの前に数字3があります.--埋め込まれたメッセージは文字列のように扱われます(有線フォーマット=2).

オプションおよび繰り返し可能要素


proto 2メッセージ定義に重複する要素([packed=true]オプションなし)がある場合、符号化メッセージには同じフィールド番号を持つキー値ペアが0つ以上あります.これらの重複する値は連続的に現れる必要はありません.他のフィールドと交差する可能性があります.解析では、他のフィールドの順序が失われますが、要素間の順序は保持されます.proto 3では、重複フィールドはpacked符号化を使用しており、以下に関連符号化が見られる.
通常、符号化メッセージには、1つ以上の重複しないフィールドのインスタンスはありません.しかし、解析器はこのような実際の状況を処理することができ、数値タイプと文字列について、同じフィールドが複数回現れると、解析器は最後の値を受け入れます.埋め込みメッセージフィールドの場合、解析器は、Message :: MergeFromメソッドを使用するように、同じフィールドの複数のインスタンスをマージします.つまり、次のインスタンスのすべての単一スカラーフィールドが、前のインスタンスの単一スカラーフィールドに置き換えられ、重複可能なフィールドが1つに直列に接続されます.これらのルールの役割は、2つの符号化されたメッセージの接続を解析した結果と、2つのメッセージをそれぞれ解析し、結果オブジェクトをマージした結果とはまったく同じです.つまり、
MyMessage message;
message.ParseFromString(str1 + str2);

に等しい
MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);

このプロパティは、タイプが分からなくても2つのメッセージをマージできるため、便利です.

重複フィールドの圧縮


Protoバージョン2.1.0では、圧縮重複フィールドが導入され、proto 2で重複フィールドとして宣言され、特殊な[packed=true]オプションが使用されます.proto 3では、デフォルトでスカラー数値タイプの重複フィールドが圧縮されます.これらの機能は重複するフィールドに似ていますが、符号化方法は異なります.ゼロ要素を含む圧縮重複フィールドは、符号化されたメッセージには表示されません.それ以外の場合、このフィールドのすべての要素は、有線タイプ2(境界)の単一キー値ペアにパッケージされます.各要素の符号化方法は、要素の前にキーがない点で通常と同じである.
たとえば、次のメッセージ・タイプがあります.
message Test4 {
  repeated int32 d = 4 [packed=true];
}

次に、Test 4を構築し、重複するフィールドdに値3、270、86942を提供すると仮定します.次に、メッセージをエンコードする形式は、次のとおりです.
22        // key (field number 4, wire type 2)
06        // payload size (6 bytes)
03        // first element (varint 3)
8E 02     // second element (varint 270)
9E A7 05  // third element (varint 86942)

元の数値タイプ(varint、32ビットまたは64ビット線型を使用するタイプ)の重複フィールドのみをpackedと宣言できます.

フィールドの順序


フィールド番号は.protoファイルでは任意の順序で使用されます.使用する順序の選択は、メッセージのシーケンス化に影響しません.
メッセージをシーケンス化する場合、既知のフィールドまたは未知のフィールドに書き込む方法に保証されない順序.シーケンス化順序は実装の詳細であり、将来特定の実装の詳細が変更される可能性があります.したがって、protocol buffer解析器は、フィールドを任意の順序で解析できる必要があります.