Protocol Buffer原理解密

背景

Protocol Buffer是Google出品的數據傳輸協議,目前已經普遍用於客戶端和服務器之間的數據交互,清晰理解Protocol Buffer原理頗有必要,本文主要解密Protocol Buffer爲何更小,更快,不瞭解Protocol Buffer的能夠看下以前對Protocol Buffer的介紹java

原理

Protocol Buffer更快,更小的主要緣由以下:算法

  • 序列化數據時,不序列化key的name,使用key的編號替代,減少數據
    例如定義以下數據結構:
message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
複製代碼

上述數據在序列化時,query,page_number以及result_per_page的key不會參與,由編號1,2,3替代,這樣在反序列的時候能夠直接經過編號找到對應的key,這樣作確實能夠減少傳輸數據,可是編號一旦肯定就不可更改bash

  • 沒有賦值的key,不參與序列化
    序列化時只會對賦值的key進行序列化,沒有賦值的不參與,在反序列化的時候直接給默認值便可
  • 可變長度編碼
    可變長度編碼,主要縮減整數佔用字節實現,例如java中int佔用4個字節,可是大多數狀況下,咱們使用的數字都比較小,使用1個字節就夠了,這就是可變長度編碼完成的事
  • TLV
    TLV全稱爲Tag_Length_Value,其中Tag表示後面數據的類型,Length不必定有,根據Tag的值肯定,Value就是數據了,TLV表示數據時,減小分隔符的使用,更加緊湊

數據結構

Protocol Buffer的數據組成方式爲TLV,數據結構圖以下: 服務器

image
其中Length不必定有,依據Tag肯定,例如int類型的數據就只有Tag-Value,string類型的數據就必須是Tag-Length-Value

數據類型

Protocol Buffer定義了以下的數據類型,其中部分數據類型已經再也不使用:數據結構

類型 釋義 備註
0 可變長度編碼 int32 int64 uint32 uint64 sint32 sint64 bool enum
1 64位長度 fixed64 sfixed64 double
2 value 的長度 string bytes message packed repeated fiels
3 Start Group 廢棄
4 End Group 廢棄
5 32位長度 fixed32 sfixed32 float

Tag

上面已經介紹了Protocol Buffer的數據結構及Tag的類型,可是Tag塊並非只表示數據類型,其中數據編號也在Tag塊中,Tag的生成規則以下:工具

(field_number << 3) | wire_type
複製代碼

其中Tag塊的後3位表示數據類型,其餘位表示數據編號post

可變長度編碼

Java中整數類型的長度都是肯定的,如int類型的長度爲4個字節,可表示的整數範圍爲-2^31——2^31-1,可是實際開發中用到的數字均比較小,會形成字節浪費,可變長度編碼就能很好的解決這個問題,可變長度編碼規則以下:ui

  • 字節最高位表示數據是否結束,若是最高位爲1,則表示後面的字節也是該數據的一部分

舉個例子: 編碼

image
其中第一個字節因爲最高位爲1,則後面的字節也是前面的數據的一部分,第二個字節最高位爲0,則表示數據計算終止,因爲Protocol Buffer是低位在前,總體的轉換過程以下:
image

10000001 00000011 ——> 00000110000001 表示的10進制數爲:2^0 + 2^7 + 2^8 = 385
經過上面的例子能夠知道一個字節表示的數的範圍0-128,上面介紹的Tag生成算法中因爲後3位表示數據類型,因此Tag中1-15編號只佔用1個字節,因此確保編號中1-15爲經常使用的,減小數據大小spa

可變長度編碼惟一的缺點就是當數很大的時候int32須要佔用5個字節,可是從統計學角度來講,通常不會有這麼大的數

負數

Java中最高位表示整數的正負,經過上面可變長度編碼介紹,最高位被用來做爲數據結束標識符了,因此無法經過最高位來表示數據的正負,使用int32或者int64表示負數的時候佔用10個字節,這是Protocol Buffer源碼中規定的,因此若是要使用負數強烈不建議使用int32和int64,建議使用sint32和sint64,sint32和sint64先使用zigZag編碼,生成的數再使用可變長度編碼,下面介紹一下zigzag編碼.

zigZag

zigzag編碼的代碼以下:

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

Zigzag(n) = (n << 1) ^ (n >> 63), n 爲 sint64 時
複製代碼

按照這種編碼方式,對應的數字以下:

image
正數和負數交叉出現,這樣編碼後的數字再使用可變長度編碼也能縮減數據的佔用

定長編碼

定長編碼其實沒什麼說的,double float等數據結構的長度是肯定的,當解析到這種類型的數據時,直接取對應長度的數據便可

案例分析

上面介紹了Protocol Buffer的原理,如今經過實例來展現分析過程,咱們定義的proto文件以下:

message Person {
  string name = 1;
  int32 id = 2;
}
複製代碼

經過Protocol Buffer提供的工具,建立對應的源文件而且設置對應的值:name=test id=1,序列化後的字節以下:

image
前面介紹過Protocol Buffer的 數據結構爲TLV,其中L不是必須的,根據T的類型來肯定
先看下第一個字節:
image
這裏字節最高位爲0,因此該Tag就用這一個字節表示,其中後3位表示類型,前面表示字段編號,因此:
file_num = 0001 = 1
type = 010 = 2
上面介紹過type=2,則後面有Length,按照可變長度編碼規則,知道表示長度的字節爲:
image
因此Length=4,則value的長度是4個字節,直接取出後面4個字節:
image
這4個字節對應的就是test
再看下一組:
image
由上面的Tag知道: file_num=2 type=0
前面介紹過type=0,後面沒有Length,直接就是value,
image
value=1,經過上面的解析能夠知道

  1. file_num=1 value=test
  2. file_num=2 value=1
    這樣解析就結束了

總結

上面介紹了Protocol Buffer的原理,解釋了爲何Protocol Buffer更快,更小,這裏再總結一下:

  1. 序列化的時候,不序列化key的name,只序列化key的編號
  2. 序列化的時候,沒有賦值的key,不參與序列化,反序列化的時候直接使用默認值填充
  3. 可變長度編碼,減少字節佔用
  4. TLV編碼,去除沒有的符號,使數據更加緊湊
相關文章
相關標籤/搜索