Google Protocol Buffers 編碼(Encoding)

Google Protocol Buffers 編碼(Encoding)

1. 概述

前三篇文章《Google Protocol Buffers 概述》《Google Protocol Buffers 入門》《Protocol Buffers 語法指南》 一步一步將你們帶入Protocol Buffers的世界,咱們已經基本可以使用Protocol Buffers生成代碼,編碼,解析,輸出級讀入序列化數據。該篇主要講述PB message的底層二進制格式。不瞭解該部份內容,並不影響咱們在項目中使用Protocol Buffers,可是瞭解一下PB格式是如何作到smaller這一層,確實是頗有必要的。Protobuf 序列化後所生成的二進制消息很是緊湊,這得益於 Protobuf 採用的很是巧妙的 Encoding 方法。html

2. 一個簡單的例子

.proto文件定義一條簡單的message:java

?
1
2
3
message Test1 {
   required int32 a = 1;
}

使用該.proto生成相應類並寫入一條message到一個文件中,這裏我寫入test.txt文件:ide

?
1
2
3
4
5
6
public static void main(String[] args) throws IOException {
     Simple simple = Simple.newBuilder().setId( 150 ).build();
     FileOutputStream output = new FileOutputStream( "abc.txt" );
     simple.writeTo(output);
     output.close();
}

使用UltraEdit打開,二進制格式查看,發現只佔用了三個字節:函數

bytes of pb message

整條message存儲只用了三個字節,甚至小於一個整形的大小,這是什麼意思?怎麼作到的?Protobuf 序列化後所生成的二進制消息很是緊湊,這得益於 Protobuf 採用的很是巧妙的 Encoding 方法。post

3. Varint

在瞭解PB encoding以前,咱們先來了解一下varint。Varint 是一種緊湊的表示數字的方法。它用一個或多個字節來表示一個數字,值越小的數字使用越少的字節數。這能減小用來表示數字的字節數。ui

Varint 中的每一個 byte 的最高位 bit 有特殊的含義,若是該位爲 1,表示後續的 byte 也是該數字的一部分,若是該位爲 0,則結束。其餘的 7 個 bit 都用來表示數字。所以小於 128 的數字均可以用一個 byte 表示。大於 128 的數字,會用兩個字節。google

例如整數1的表示,僅需一個字節:編碼

0000 0001url

例如300的表示,須要兩個字節:spa

1010 1100 0000 0010

採 用 Varint,對於很小的 int32 類型的數字,則能夠用 1 個 byte 來表示。固然凡事都有好的也有很差的一面,採用 Varint 表示法,大的數字則須要 5 個 byte 來表示。從統計的角度來講,通常不會全部的消息中的數字都是大數,所以大多數狀況下,採用 Varint 後,能夠用更少的字節數來表示數字信息。

下圖演示了 Google Protocol Buffer 如何解析兩個 bytes。注意到最終計算前將兩個 byte 的位置相互交換過一次,這是由於 Google Protocol Buffer 字節序採用 little-endian 的方式。

PB Varint

 

 

 

 

 

 

4. Message 格式

消息通過序列化後會成爲一個二進制數據流,該流中的數據爲一系列的 Key-Value 對。以下圖所示:

Message Buffer

採用這種 Key-Pair 結構無需使用分隔符來分割不一樣的 Field。對於可選的 Field,若是消息中不存在該 field,那麼在最終的 Message Buffer 中就沒有該 field,這些特性都有助於節約消息自己的大小。

 

二進制格式的message使用數字標籤做爲key,Key 用來標識具體的 field,在解包的時候,Protocol Buffer 根據 Key 就能夠知道相應的 Value 應該對應於消息中的哪個 field。

將 message編碼後,key-values被編碼成字節流存儲。在message解碼時,PB 解析器會跳過(忽略)不可以識別的字段,因此,message即便增長新的字段,也不會影響老程序代碼,由於老程序代碼根本就不能識別這些新添加的字段。 爲此,該處,key須要特殊設計。

上邊咱們說,「二進制格式的message使用數字標籤做爲key」,此處的數字標籤,並不是單純的數字標籤,而是數字標籤與傳輸類型的組合,根據傳輸類型可以肯定出值的長度。

key的定義:

(field_number << 3) | wire_type

能夠看到 Key 由兩部分組成。第一部分是 field_number,第二部分爲 wire_type。表示 Value 的傳輸類型。也就是說,key中的後三位,是值得傳輸類型。有關移位操做簡單知識,能夠參見:Java位操做基本知識

Wire Type 可能的類型以下表所示:

Type Meaning Used For
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimi string, bytes, embedded messages, packed repeated fields
3 Start group Groups (deprecated)
4 End group Groups (deprecated)
5 32-bit fixed32, sfixed32, float

5. 分析產生數據

在第二部分簡單的例子中,寫入message後,咱們看到最終輸出文件中包含三個數字:08 96 01,這是如何得來的呢?

如圖:

至此咱們知道數字標籤是1,值類型爲varint。使用第四部分咱們分析的,來解碼96 01,即爲150:

?
1
2
3
4
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

注意:數值部分,低位在前,高位在後。

6. 其餘數值類型

6.1 有符號整數

細 心的讀者或許會看到在 Type 0 所能表示的數據類型中有 int32 和 sint32 這兩個很是相似的數據類型。Google Protocol Buffer 區別它們的主要意圖也是爲了減小 encoding 後的字節數。這部分,主要是針對負數來設計的。

在計 算機內,一個負數通常會被表示爲一個很大的整數,由於計算機定義負數的符號位爲數字的最高位。若是採用 Varint 表示一個負數,那麼必定須要 10 個 byte長度。爲此 Google Protocol Buffer 定義了 sint32 這種類型,採用 zigzag 編碼。將全部整數映射成無符號整數,而後再採用varint編碼方式編碼,這樣,絕對值小的整數,編碼後也會有一個較小的varint編碼值。

Zigzag映射函數爲:

Zigzag(n) = (n << 1) ^ (n >> 31), n爲sint32時

Zigzag(n) = (n << 1) ^ (n >> 63), n爲sint64時

按照這種方法,-1將會被編碼成1,1將會被編碼成2,-2會被編碼成3,以下表所示:

Signed Original Encoded As
0 0
-1 1
1 2
-2 3
2 4
-3 5
2147483647 4294967294
-2147483648 4294967295

6.2 Non-varint 數字

Non-varint數字比較簡單,double 、fixed64 的線路類型爲 1,在解析式告訴解析器,該類型的數據須要一個64位大小的數據塊便可。同理,float和fixed32的線路類型爲5,給其32位數據塊便可。兩種狀況下,都是高位在後,低位在前。

6.3 String

線路類型爲2的數據,是一種指定長度的編碼方式:key+length+content,key的編碼方式是統一的,length採用varints編碼方式,content就是由length指定長度的Bytes。定義以下的message格式:

?
1
2
3
4
5
message Test2 {
  
required string b = 2;
  
}

設置該值爲"testing",二進制格式查看:

12 07 74 65 73 74 69 6e 67

紅色字節爲「testing」的UTF8代碼。

此處,key是16進製表示的,因此展開是:

12 -> 0001 0010,後三位010爲wire type = 2,0001 0010右移三位爲0000 0010,即tag=2。

length此處爲7,後邊跟着7個bytes,即咱們的字符創"testing"。

6.4 嵌套message

定義以下嵌套消息:

?
1
2
3
message Test3 {
   required Test1 c = 3;
}

同第二部分同樣,設置字段爲整數150,編碼後的字節爲:

1a 03 <span style="color: red;">08 96 01</span>

咱們發現,後三個字節跟咱們第一個例子中的一摸同樣(08 96 01),他們前邊有一個長度限制03,課件嵌套消息跟string是一摸同樣的,其wire type 也爲2。

6.5 wire type = 三、4

該兩個字段已經廢棄再也不使用,故忽略吧~

7. 可選字段和重複字段

假 如定義的message中有repeated元素而且該聲明後並未使用[packed=true]選項,編碼後的message有一個或者多個包含相同 tag數字的key-value對。這些重複的value不須要連續的出現;他們可能與其餘的字段間隔的出現。儘管他們是無序的,可是在解析時,他們是需 要有序的。

對於可選字段,編碼後的message中,擁有該數字標籤的key-value對無關緊要。

一般,編碼後的 message,其required字段和optional字段最多隻有一個實例。可是解析器卻須要處理多餘一個的狀況。對於數字類型和string類 型,若是同一值出現屢次,解析器接受最後一個它收到的值。對於內嵌字段,解析器合併(merge)它接收到的同一字段的多個實例。就如MergeFrom 方法同樣,全部單數的字段,後來的會替換先前的,全部單數的內嵌message都會被合併(merge),全部的repeated字段,都會串聯起來。這 樣的規則的結果是,解析兩個串聯的編碼後的message,與分別解析兩個message而後merge,結果是同樣的。例如:

?
1
2
MyMessage message;
message.ParseFromString(str1 + str2);

這種作法,等價於:

?
1
2
3
4
MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);

這種方法有時是很是有用的。好比,即便不知道message的類型,也可以將其合併。

7.1 設置了[packed = true]的repeated字段

在 2.1.0後,PB引入了該種類型,其與repeated字段同樣,只是在末尾聲明瞭[packed=true]。相似repeated字段卻又不一樣。對 於packed repeated字段,若是message中沒有賦值,則不會出如今編碼後的數據中。不然的話,該字段全部的元素會被打包到單一一個key-value對 中,且它的wire type=2,長度肯定。每一個元素正常編碼,只不過其前沒有標籤。例若有以下message類型:

?
1
2
3
message Test4 {
     repeated int32 d = 4 [packed= true ];
}

構造一個Test4字段,而且設置repeated字段d兩個值:三、270和86942,編碼後:

?
1
2
3
4
5
6
7
8
9
22 // tag 0010 0010(field number 010 0 = 4, wire type 010 = 2)
  
06 // payload size (設置的length = 6 bytes)
  
03 // first element (varint 3)
  
8E 02 // second element (varint 270)
  
9E A7 05 // third element (varint 86942)

僅有原子數字類型(varint, 32-bit, or 64-bit)能夠被聲明爲「packed」

有一點須要注意,對於packed的repeated字段,儘管一般沒有理由將其編碼爲多個key-value對,編碼器必須有接收多個key-pair對的準備。這種狀況下,payload 必須是串聯的,每一個pair必須包含完整的元素。

8. 字段順序

簡單來講只有兩點:

  1. 編碼/解碼與字段順序無關,這一點由key-value機制就能保證
  2. 對於未知的字段,編碼的時候會把它寫在序列化完的已知字段後面。

推薦閱讀順序,但願給你帶來收穫~

Google Protocol Buffers 概述

Google Protocol Buffers 入門

Protocol Buffers 語法指南

Google Protocol Buffers 編碼(Encoding)

相關文章
相關標籤/搜索