Protobuf3 編解碼

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

1.什麼是 Varint
(1).Varint 是一種緊湊的表示數字的方法,它用一個或多個字節來表示一個數字,值越小的數字使用越少的字節數。這能減小用來表示數字的字節數。
好比對於 int32 類型的數字,通常須要 4 個 byte 來表示。可是採用 Varint,對於很小的 int32 類型的數字,則能夠用 1 個 byte 來表示。固然凡事都有好的也有很差的一面,採用 Varint 表示法,大的數字則須要 5 個 byte 來表示。
(2).Varint 中的每一個 byte 的最高位 bit 有特殊的含義,若是該位爲 1,表示後續的 byte 也是該數字的一部分,若是該位爲 0,則結束。其餘的 7 個 bit 都用來表示數字。
所以小於 128 的數字均可以用一個 byte 表示。大於 128 的數字,好比 300,會用兩個字節來表示:1010 1100 0000 0010


函數

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

圖 6. Varint 編碼

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

圖 7. 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即便增長新的字段,也不會影響老程序代碼,由於老程序代碼根本就不能識別這些新添加的字段。
上邊咱們說,「二進制格式的message使用數字標籤做爲key」,此處的數字標籤,並不是單純的數字標籤,而是數字標籤與傳輸類型的組合,根據傳輸類型可以肯定出值的長度。spa

key的定義:code

(field_number << 3) | wire_type

能夠看到 Key 由兩部分組成,第一部分是 field_number,第二部分爲 wire_type。表示 Value 的傳輸類型,也就是說:key中的後三位,是值的wire_type類型。
Wire Type 類型以下表所示:blog

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

舉個例子來分析protobuf數據編碼和解碼,以下所示:ci

message Test1 {
    required int32 a = 1;    
}
//.......protobuf讀寫操做..........
Test1 test;
test.set_a(150);
//.....將數據序列化到文件.....

寫入message後,用UltraEdit打開,二進制格式查看,咱們看到最終輸出文件中包含三個數字:08 96 01(十六進制),這是如何得來的呢?
1.首先來解析tag字符串

2.至此咱們知道數字的field_number=1,值類型爲varint。根據上面講解來解碼96 01,即爲150:string

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

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

 

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

圖 8. ZigZag 編碼

使用 zigzag 編碼,絕對值小的數字,不管正負均可以採用較少的 byte 來表示,充分利用了 Varint 這種技術。
其餘的數據類型,好比字符串等則採用相似數據庫中的 varchar 的表示方法,即用一個 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

 

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

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

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"。

字段順序簡單來講只有兩點:    編碼/解碼與字段順序無關,這一點由key-value機制就能保證    對於未知的字段,編碼的時候會把它寫在序列化完的已知字段後面

相關文章
相關標籤/搜索