本文檔介紹了protocol buffer訊息的二進位格式。在你的應用程式中使用protocol buffers的時候,你不需要理解這些,但是對於你想知道不同的protocol buffer格式如何影響你的訊息編碼的大小是非常有用的。
一個簡單的訊息
假設你有如下的一個簡單的訊息定義:
message Test1 {
required int32 a = 1;
} 在一個應用程式中,你建立了一個Test1訊息並且設定a的值為150。然後你將這個訊息序列化到一個輸出資料流。如果你想檢車編碼後的訊息,你將看到如下的三個位元組:
08 96 01
Base 128 Varints
為了理解protocol buffer的編碼,首先需要理解varints。Varints是一個將整數序列化成一個或多個位元組的方法。數字越小佔用的位元組數也越小。
一個varint中的每個位元組,除了最後一個位元組,都要設定一個most significant bit (msb)用來標識接下來還有位元組。每個位元組的低7位用來儲存7個位元組表示的數位二進位補碼,least significant group first。
例如,數字1,它是單個位元組,所以不需要設定msb:
0000 0001如果是300,就要複雜一點:
1010 1100 0000 0010你是如何推斷這是300呢?首先你丟掉每個位元組的msb,因為msb只是用來告訴我們是否到達數位末尾:
1010 1100 0000 0010
→ 010 1100 000 0010接著反轉兩組7位位元組,因為varints儲存數位原則是the least significant group first。然後可以計算最終值了:
000 0010 010 1100
→ 000 0010 ++ 010 1100
→ 100101100
→ 256 + 32 + 8 + 4 = 300
訊息結構
一個protocol buffer訊息是一系列的key-value對。一個訊息的二進位版本就是使用欄位的數字作為key——每個欄位的名字和聲明的類型在解碼結束的時候通過引用訊息的類型定義(比如.proto檔案)來決定。
當一個訊息被編碼的時候,keys和values被連結成一個位元組流。當訊息被解碼的時候,解析器需要跳過它不能識別的欄位。這樣的話,新的欄位可以被添加到一個訊息中,而不會中斷不知道這些新欄位的舊程式。每個key-value對中的key實際上包含了兩個值——來自.proto檔案的欄位數字,加上一個類型用來提供足夠的資訊決定接下來值得長度。
可用的wire類型如下:
int32, int64, uint32, uint64, sint32, sint64, bool, enum Type Meaning Used For
0 Varint
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
編碼成流的訊息中的每個key是一個varint,其值為(field_number<<3)|wire_type——換句話說,數位最後三個bit用來儲存wire type的。
現在再看一個簡單的例子。你現在已經知道流中第一個數字總是一個varint key,這裡是08,或者(丟掉msb):
000 1000你通過後三位bit得到wire type是0,然後右移三位得到欄位數字是1。所以你現在知道tag是1,隨後的值是一個varint。使用前面的varint解碼知道,我們可以知道隨後的兩個位元組儲存的值是150。
96 01 = 1001 0110 0000 0001
→ 000 0001 ++ 001 0110 (drop the msb and reverse the groups of 7 bits)
→ 10010110
→ 2 + 4 + 16 + 128 = 150
更多的實值型別
有符號整數
所有的protocol buffer類型為0的被編碼成varints。但是,當編碼負數的時候,有符號整型(sint32和sint64)和標準整型(int32和int64)是有很大區別的。如果你使用int32或int64作為一個負數的類型,其對應的varint總是10個位元組的長度——它像一個非常大的不帶正負號的整數被對待。如果你使用有符號類型,其對應的varint會使用ZigZag編碼,這樣會更高效。
ZigZag編碼將有符號整數映射到不帶正負號的整數,所以擁有較小絕對值的數字(比如-1)也擁有較小的varint編碼值。其做法是正數和負數之間反覆地"zig-zags",所以-1編碼成1,1編碼成2,-2編碼成3,依此類推,如下表所示:
Signed Original Encoded As
0 0
-1 1
1 2
-2 3
2147483647 4294967294
-2147483648 4294967295
換句話說,每一個值n對於sint32被編碼如下:
(n << 1) ^ (n >> 31)對於sint64,被編碼:
(n << 1) ^ (n >> 63)注意(n>>31)部分是一個算術移位。換句話說,這個移位的結果數字要麼所有的位全0(n是正數),要麼全1(n是負數)。
當sint32或sint64被解析的時候,其值被解碼成有符號的原始值。
非varint數字
非varint數字類型也非常簡單——double和fixed64其wire type為1,告訴解析器擷取一個固定的64位的資料;float和fixed32其wire type為5,告訴解析器擷取一個32位的資料。這兩種情況下,其對應的值都是以小端位元組順序儲存的。
字串
wire type為2意味著其值是一個varint編碼長度,後面接著的是指定位元組數的資料。
message Test2 {
required string b = 2;
}設定b的值為"testing",會得到:
12 07 74 65 73 74 69 6e 67紅色的位元組部分是UTF-8編碼的"testing"。key是0x12->tag=2,type2。長度的值是7,隨後的7個位元組就是我們的字串。
嵌套的訊息
如下的例子中有一個嵌套的訊息:
message Test3 {
required Test1 c = 3;
}編碼後的結果如下,Test1的欄位a的值設為150:
1a 03 08 96 01可以看到,後三個位元組和我們前面第一個例子相同(08 96 10),表示數字150,其前面是數字3,嵌套訊息被當做字串來看待(wire type=2)。
可選和重複元素
如果你的訊息中有repeated元素(沒有[packed=true]選項),編碼後的訊息有0個或多個key-value對有相同的tag數字。這些重複的值不需要連續的出現,他們可能與其他的欄位交錯出現。元素的順序在解析的時候確定。
如果元素是optional,編碼後的訊息可能有或沒有key-value對。
正常情況下,一個編碼的訊息擁有的一個optional或required欄位的執行個體不會超過一個。但是,解析器也會處理這種情況。對於數實值型別和字串,如果同樣的值出現了多次,解析器只接受最後的值。對於嵌套的訊息欄位,解析器合并相同欄位的多個執行個體,就像Message::MergeFrom方法做的一樣——那就是,後面的執行個體的單個欄位會替換掉前面出現的,單個嵌套訊息被合并,重複欄位被連結。這些規則的效果就是解析串聯出現的兩個編碼的訊息產生的結果和單獨解析兩個訊息然後合并它們的結果是相同的。樣本如下:
MyMessage message;
message.ParseFromString(str1 + str2);等價於:
MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);
Packed Repeated欄位
版本2.1.0中介紹了packed repeated欄位,同時定義了repeated和[packed=true]選項。一個包含0個元素的packed repeated欄位不會出現在編碼訊息中,除此之外,欄位的所有元素被打包到一個key-value對中,用wire type 2標識。每個元素按照自身的類型編碼。
例如,假設你有如下的訊息類型:
message Test4 {
repeated int32 d = 4 [packed=true];
}構造一個Test4,d欄位包含的值有3,270,86942。然後,編碼的結果如下:
22 // tag (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)僅僅repeated的基礎數實值型別(varint, 32-bit, 64-bit)才可以聲明"packed"。
欄位順序
你可以在一個.proto檔案中以任何的順序來使用欄位數字,當一個訊息被序列化的時候,它的已知的欄位會按照欄位數字順序連續寫入。這個允許解析代碼依賴於欄位數字做最佳化。但是,protocol buffer的解析器應該能夠以任何順序解析欄位,因為並不是所有的訊息是通過序列化一個對象建立的。