Protobuf編碼指南

這個文檔會介紹protocol buffer的二進制有線格式(binary wire format)。你並不是需要理解這些後才能在應用裏使用protocol buffer,但是當你想知道不同的protocol buffer格式是如何影響編碼後的消息體的體積時,這些知識會非常有用。

一個簡單的消息

假設有一個非常簡單的消息定義:

message Test1 {
  optional int32 a = 1;
}

在應用中,你創建了一個Test1消息並把a設置爲150。然後你把消息序列化到輸出流中,如果你能查看編碼後的消息,你會看到三個字節:

08 96 01

到目前爲止,如此小而且都是數字-但是這是什麼意思呢?繼續往下看

Varint編碼

要理解上面protocol buffer編碼的數據,你需要先理解vaintsVarints是一種使用一個或多個字節編碼整數的方法。較小的數字使用較少的字節。

除了最後一個字節外,varint編碼中的每個字節都設置了最高有效位(most significant bit - msb)–msb爲1則表明後面的字節還是屬於當前數據的,如果是0那麼這是當前數據的最後一個字節數據。每個字節的低7位用於以7位爲一組存儲數字的二進制補碼錶示,最低有效組在前,或者叫最低有效字節在前。這表明varint編碼後數據的字節是按照小端序排列的。

舉例來說,對於數字1-它佔用單個字節,所以字節的最高位上是0

0000 0001

對於數字300會有一點複雜,它佔用倆個字節

1010 1100 0000 0010

那麼是怎麼計算出來是300的呢?首先你需要把每個字節的msb去掉,因爲它只用來告訴我們是否已經到達數字的最後一個字節(本例的varint佔用倆個字節所以第一個字節的msb爲1)

 1010 1100 0000 0010
→ 010 1100  000 0010

將兩組7位反轉,因爲你記得,varint存儲的數字最低有效組在前。然後,將它們連接起來以獲得最終值

000 0010  010 1100 (去掉最高有效位,並反轉7位組)
→  000 0010 ++ 010 1100
→  100101100
→  256 + 32 + 8 + 4 = 300

:varint編碼理解起來有點難,可以看之前寫的varint編碼原理解析

消息的組成

如你所知,一個protocol buffer是一系列鍵值對。消息的二進制格式只使用消息字段的字段編號作爲鍵--字段名和聲明的類型只能在解析端通過引用參考消息類型定義(即.proto文件)才能確定。

當一個消息被編碼時,鍵和值會被連接放入字節流中。當消息被解碼時,分析器需要能夠跳過未識別的字段。這樣,新加入消息的字段就不會破壞不知道他們存在的那些老程序。爲此,有線格式消息中每個對的“鍵”實際上是兩個值-.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 獲得--也就是說字節的後三位存儲的是有線類型。

現在讓我們再回到上面的消息示例。你現在知道字節流中的首個字節永遠都是一個varint鍵,在我們的例子中它是08或者下面的二進制(去掉了msb)。

000 1000

通過後三位得出有線類型(0),然後右移三位得到字段編號(1)。現在你知道字段的編號是1對應的值是一個varint。使用前面學到的解碼varint的知識,你可以看到下面的兩個字節存儲着值150。

96 01 = 1001 0110  0000 0001
       → 000 0001  ++  001 0110 (去掉最高有效位,並反轉7位組)
       → 10010110
       → 128 + 16 + 4 + 2 = 150

更多值類型

有符號整數

就像你在上一部分看到的那樣,protocol buffer中所有與有線類型0關聯的類型都會被編碼爲varint。但是,在編碼負數時,帶符號的int類型(sint32和sint64)與“標準” int類型(int32和int64)之間存在着巨大區別。如果將int32或int64用作負數的類型,則結果varint總是十個字節長––實際上,它被視爲一個非常大的無符號整數。如果使用帶符號類型(sint32和sint64)之一,則生成的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編碼的數字來說比較簡單--doublefixed64使用有線類型1,這會告訴解析器期望固定的64-bit的數據塊。相似地floatfixed32使用有線類型5,這會告訴解析器期望固定的32-bit數據塊。這兩種情況都是使用小端序排列字節存儲數據的。

字符串

有線類型2(長度分隔)表示該值是varint編碼的長度值,後跟長度值指定數量的數據字節。

message Test2 {
  optional string b = 2;
}

設置b的值爲"testing"後消息對應的二進制有線格式爲

12 07 <font color="red">74 65 73 74 69 6e 67</font>

紅色的字節是UTF-8編碼後的"testing"

這裏的鍵是0x12→0001 0010→字段號= 2,類型=2(第一個字節的後三位表示有線類型的編號,然後右移三位變成000 0010得到字段號)。值中的varint表示的數據字節長度是7,如你所見我們在它後面找到的七個字節–就是解析器要找的字符串。

內嵌消息

下面是一個擁有內嵌消息的消息定義Test3,內嵌的消息類型是我們上面示例中定義的Test1

message Test3 {
  optional Test1 c = 3;
}

下面則是內嵌的Test1中的a設置爲150,Test3`被編碼後的版本

1a 03 <font color="red">08 96 01</font>

如你所見,最後三個字節和我們第一個例子編碼後的結果一樣(08 96 01),在他們之前是數字3,--內嵌消息會像字符串一樣被對對待(有線格式=2)。

可選和可重複元素

如果proto2消息定義具有重複的元素(不帶[packed = true]選項),則編碼消息具有零個或多個具有相同字段編號的鍵值對。這些重複的值不必連續出現。它們可能與其他字段交錯。解析時,元素之間的順序會保留下來,儘管其他字段的順序會丟失。在proto3中,重複字段使用packed編碼,可以在下面看到相關編碼。

通常,編碼消息永遠不會有一個以上非重複字段的實例。但是,解析器能處理這種實際情況,對於數字類型和字符串,如果同一字段多次出現,則解析器將接受它看到的最後一個值。對於嵌入式消息字段,解析器將合併同一字段的多個實例,就像使用Message :: MergeFrom方法一樣-也就是說,後一個實例中的所有單個標量字段將替換前一個實例中的單個標量字段,可重複字段會被串聯到一塊。這些規則的作用是,解析兩個編碼的消息的連接所產生的結果與您分別解析兩個消息併合並結果對象的結果完全相同。也就是說:

MyMessage message;
message.ParseFromString(str1 + str2);

等同於

MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);

這個特性有時很有用,因爲即使您不知道它們的類型,也允許你合併兩個消息。

壓縮重複字段

proto版本2.1.0引入了壓縮重複字段,在proto2中聲明爲重複字段,並使用特殊的[packed = true]選項。在proto3中,默認情況下壓縮標量數字類型的重複字段。這些功能類似於重複的字段,但編碼方式不同。包含零元素的壓縮重複字段不會出現在編碼的消息中。否則,該字段的所有元素都將打包爲有線類型爲2(定界)的單個鍵值對。每個元素的編碼方式與通常相同,不同之處在於元素之前沒有鍵。

舉例來說,你有以下消息類型:

message Test4 {
  repeated int32 d = 4 [packed=true];
}

現在假設您構造一個Test4,爲重複的字段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解析器必須能夠以任何順序解析字段。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章