1. 程式人生 > >protocol buffer編碼格式分析

protocol buffer編碼格式分析

1.protocol buffer編碼背景

Protocol Buffer(PB)是google 的一種資料交換的格式,它獨立於語言,獨立於平臺。可以理解為一種信源編碼方式,就是將待傳輸的信源符號經過某種變換,轉換成碼流進行傳輸的這個變換過程。信源編碼可分為兩類:有損編碼與無損編碼,PB屬於無損編碼,在無損編碼中,又分為定長編碼和變長編碼,定長編碼就是一個符號變換後的碼字的位元長度是固定的,比如ASCII、Unicode都是定長編碼,碼字是8位元,16位元。變長編碼則是將信源符號對映為不同的碼字長度,比如Huffman編碼,PB編碼

PB也可以看作是一種協議,主要用於物件序列化。那麼,如何記錄一個物件的變數值呢?目前典型的格式有XML和JSON。這兩種方式都有兩個共同特點,即自描述特性以及文字描述

。自描述是指變數名也包含在格式中。而PB不包含變數名本身,同時採用二進位制編碼,通訊底層的協議一般均為二進位制,具有解析速度快、佔用空間小的優點,缺點是缺乏可讀性了。PB編碼充分利用二進位制編碼和可變長度的特點,節約物件的儲存空間。

2.protocol buffer的二進位制編碼格式

2.1 對比定長編碼和文字結構編碼,瞭解protocol buffer的編碼格式是如何影響到編碼之後的檔案大小。

整數的編碼

int型定長編碼:4個位元組
PD編碼(變長編碼):pd為每個整數編碼後還是整數個位元組,但位元組個數可能不同。將每個位元組拿出1位元最高位的那個位元MSB(Most Significant Bit)來作為邊界的標記(編碼是否為最後一個位元組),1表示還沒有到最後一個位元組,0表示到了最後一個位元組。

0xxx xxxx表示某個整數編碼後的結果是單個位元組,因為MSB=0;
1xxx xxxx 0xxx xxxx表示某個整數編碼後的結果是2個位元組,因為前一個位元組的MSB=1(編碼結果未結束),後一個位元組的MSB=0;
同理,三個位元組、四個位元組都用這種方法來表示邊界.

舉例:
0000 0001表示整數1;
1010 1100 0000 0010表示兩個位元組的結果;
將兩位元組的MSB去掉為:0101100 0000010,
PB對於多個位元組的情況採用低位元組優先,即後面的位元組要放在高位,於是拼在一起的結果為:00000100101100表示300這個整數值。(其實就是將數字的二進位制補碼的每7位分為一組, 低7位先輸出,編碼在前面,在輸出下一組,依次類推)

物件的編碼

當物件包含多個變數是,以一個類的定義為例:

message Test1{
     IntFlag= 150;
     StringFlag="testing";
}

利用輸出流序列化這些資訊,當我們開啟這個序列化檔案的時候可以看到一共利用3個bytes表示的這個Message:08 96 01
PD序列編碼主要有以下協議措施減少儲存:
1)序號key替代變數名:
PB採用了“編號+對應變數值”的這種形式來序列化。因為編號肯定是唯一的,所以這種形式其實就是一系列Key-Value對,Key就是編號,Value就是編號對應變數的值。

分析:採用編號替代json等文字格式中的變數名可以省很大空間(實際應用中變數名可能很長);之所以保留編號,而沒有直接取消按值順序傳遞,因為某些值可能為空,沒必要傳遞過去(預設值),就只需要傳遞一部分即可。而1、2、3、4這些編號都不記錄的話,就必須所有的都傳遞過去,反而並不節省空間。(因為key為整數,對小整數的pd編碼是很節約空間的,一般需要1個位元組)

2) 1bit定結束邊界:
關於key的編碼:對於key(小整數)我們採用上面的整數編碼,當整數編碼較大需要多個bytes時,採用整數編碼提到的,將每個位元組拿出1位元最高位的那個位元MSB(Most Significant Bit)來作為邊界的標記。
3) 3bit標記value型別;
關於value的編碼:Value也面臨和Key一樣的問題,首先也需要知道Value的結果有多長,是不是也採用類似的方法呢,這樣就會有些難辦。比如Value如果是一個字串,可能很長,每個位元組都拿出一位元來這麼弄,浪費且不說,而且字串本身就是一個一個位元組的,完全被打亂了,解碼的時候速度會降低。所以Value值最好一整個的放在一起。

最簡單的一種思路是,關於Value長度的指示可以放在Key和Value之間。因為長度本身也是一個整數,就用前面那種方法進行編碼即可,在解碼時,先得到Key,然後後面跟著Value的長度,解析得到Value長度後,再解析Value值
能不能更加節省呢?PB更加高明之處就在於此。通過觀察可以知道,在程式設計時,很多變數都是一個整數(int,int64等等),對於整數,無需長度指示,按照整數編碼即可。但不指示的話,怎麼知道後面是個整數呢?

PB於是把Key增加了3個bit,記錄後面的Value的型別wire_type(3位可以表示的型別有限,對型別粗分類),key的最終表示為:(Key << 3) | wire_type

型別 意義 用途
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 group開始 group(已經啟用)
4 group結束 group(已經棄用)
5 32-bit fixed32, sfixed32, float

以上面的序列編碼08 96 01為例:
因為第一個位元組是Key的一個整型變數(08),去除MSB為:0001000
從這個數字的低三位我們取出value的型別(0),然後向右邊移動三位可以得到它的編號(1)。所以現在我們知道tag為1,接下來的數是一個varint,接下來用varint譯碼方法(比如整型譯碼,不需儲存value長度)進行譯碼:

96 01 = 1001 0110  0000 0001
   → 000 0001  ++  001 0110 (丟棄MSB,按照7bits進行反轉,低位元組優先編碼)
   → 10010110
   → 2 + 4 + 16 + 128 = 150

這樣,08 96 01這三個位元組就表示第一個變數值為整數150。

另一個例子:12 07 74 65 73 74 69 6e 67
第一個位元組12去除MSB為:0010010, 後三位010表示wire type=2,前四位0010表示第2個變數。
因為wire type=2,表示Value是string, bytes等Length-delimited,接下來的數記錄了Value的長度。
07的二進位制:0000 0111,因為MSB=0,所以是最後一個位元組,其值為0000111,即為7,表示Value的長度為7,也就是後面的7個位元組:74 65 73 74 69 6e 67
這7個位元組假如是string,則為“testing”(ASCII碼)
於是知道,傳遞的是第二個變數,且值為“testing”。

如果上面的例子串起來:08 96 01 12 07 74 65 73 74 69 6e 67
就表示物件的第一個整數值為150,第二個變數的字串為“testing”。

假如用JSON的話,就類似於這樣:
{"IntFlag":"150","StringFlag":"testing"}