【轉】轉自:序列化筆記之一:Google的Protocol Buffer格式分析html
從公開介紹來看,ProtocolBuffer(PB)是google 的一種數據交換的格式,它獨立於語言,獨立於平臺。做爲一個學了多年通訊的人,ProtocolBuffer在我看來是一種信源編碼。所謂信源編碼,就是將待傳輸的信源符號通過某種變換,轉換成碼流進行傳輸的這個變換過程。信源編碼可分爲兩類:有損編碼與無損編碼,PB天然是屬於無損編碼,在無損編碼中,又分爲定長編碼和變長編碼,定長編碼就是一個符號變換後的碼字的比特長度是固定的,好比ASCII、Unicode都是定長編碼,碼字是8比特,16比特。變長編碼則是將信源符號映射爲不一樣的碼字長度。典型的是Huffman編碼。PB也屬於這一類。post
從另外一個角度來看,也能夠看作一種協議。不管如何,PB的信源就是整數、Float值、字符串等等程序設計中常見的變量,主要用於對象序列化。那麼,如何記錄一個對象的變量值呢?目前典型的格式有XML和JSON。這兩種方式都有兩個共同特色,即自描述特性以及文本描述。自描述是指變量名也包含在格式中。而PB則去除了這一條,同時採用二進制編碼,通訊底層的協議通常均爲二進制,具備解析速度快、佔用空間小的優勢,缺點嘛,固然是缺少可讀性了。ui
咱們來看看PB的格式是怎麼設計的。google
先考慮最簡單的狀況,要對一個整數值進行編碼,怎麼辦?最簡單的方式固然是直接把這個int值看做4字節,這4字節就做爲整數的編碼便可。不過這對於每一個整數須要4個字節,PB因而考慮用變長編碼,若是用變長編碼的話,每一個整數的編碼長度可能不同,如何區分邊界呢?這是一個核心問題。編碼
PB認爲每一個整數編碼後仍是整數個字節,但字節個數可能不一樣。整數個字節簡化了一些設計,並將每一個字節拿出1比特來做爲邊界的標記。一個字節有8比特,拿出最高位的那個比特MSB(Most Significant Bit)來,這個比特用於記錄這個字節是不是編碼結果的最後一個字節。若是等於1,則表示尚未到最後一個字節,不然表示到了最後一個字節,因而每一個整數編碼後的結果都是這樣子:url
0xxx xxxx表示某個整數編碼後的結果是單個字節,由於MSB=0;設計
1xxx xxxx 0xxx xxxx表示某個整數編碼後的結果是2個字節,由於前一個字節的MSB=1(編碼結果未結束),後一個字節的MSB=0;code
同理,三個字節、四個字節都用這種方法來表示邊界。htm
邊界弄好了,裏面的內容就能夠填了,xxxx這些內容填什麼呢?就填整數的補碼。至於什麼是補碼,處處都有資料。對象
舉例:
0000 0001表示整數1;
1010 1100 0000 0010表示兩個字節的結果,將兩字節的MSB去掉爲:0101100 0000010,PB對於多個字節的狀況採用低字節優先,即後面的字節要放在高位,因而拼在一塊兒的結果爲:
00000100101100
表示300這個整數值。
整數的編碼解決了,這只是一個很簡單的例子,對於一個對象,裏面包含多個多個變量,怎麼編碼呢?好比一個類的定義爲:
class Test
{
int A;
float B;
string C;
double D;
}
在JSON等格式中,使用文本編碼,看起來就很簡單,好比:
{"A":"46","B":"13.45","C":"aaaa","D":"3.78"}
PB的設計者認爲"A","B","C"等等這些變量名不該該包含在傳輸消息中,由於這個Test對象可能會被反覆傳輸,每一次傳輸都要傳輸"A","B","C"這些標記,但實際上這些標記是不會變的,只有值會變,因此頂多傳一次就好了,那麼,PB的設計就換了一種思路,在通訊雙方都保持一份文檔,記錄了"A","B","C"的編號,好比:
"A","B","C",「D」的編號分別爲一、二、三、4。因而在序列化的時候,只須要傳輸下面的信息:
1:"46",2:"13.45",3:"aaaa",4:"3.78"
這個例子雖然看起來並不起眼,可是程序裏面不少時候變量比較長,其實仍是能節省不少空間的,只要把這個信息傳過去,對方自己保留了一份編號文檔,因而能夠反序列化了。
那麼,按照這種邏輯,是否是一、二、三、4這些編號都不必傳了,直接按照某種約定順序發過去就好了不是也能夠?對方照着順序解碼便可。
但PB仍是保留了一、二、三、4這些編號信息,由於某些值可能爲空,不必傳遞過去,甚至在程序中,一個對象中的不少變量值其實都是缺省值,或者無所謂的值,只有一部分須要傳遞過去,這時候,就只須要傳遞一部分便可。而一、二、三、4這些編號都不記錄的話,就必須全部的都傳遞過去,反而並不節省空間。
最終,PB採用了「編號+對應變量值」的這種形式來序列化。由於編號確定是惟一的,因此這種形式其實就是一系列Key-Value對,Key就是編號,Value就是編號對應變量的值。
編碼結果就呈現爲:
Key 1的編碼--Value 1的編碼--Key 2的編碼--Value 2的編碼--。。。
由於Key都是整數,因此就利用咱們前面看到的整數編碼。由於Key都是從一、二、三、。。開始的,因此對小整數編碼結果如何短的話,就能節省空間,從前面能夠看出,小整數的編碼結果確實短,好比大多數小整數只佔一個字節。
而且上面的編碼結果也能對Key記錄邊界(最後一個字節的MSB=0,前面的字節MSB=1),也就是說可以知道Key的長度。Key後面跟的就是Value,那麼,Value也面臨和Key同樣的問題,首先也須要知道Value的結果有多長,是否是也採用相似的方法呢,這樣就會有些難辦。好比Value若是是一個字符串,可能很長,每一個字節都拿出一比特來這麼弄,浪費且不說,並且字符串自己就是一個一個字節的,徹底被打亂了,解碼的時候速度會下降。因此Value值最好一整個的放在一塊兒。
怎麼辦呢?最簡單的一種思路是,關於Value長度的指示能夠放在Key和Value之間。由於長度自己也是一個整數,就用前面那種方法進行編碼便可,在解碼時,先獲得Key,而後後面跟着Value的長度,解析獲得Value長度後,再解析Value值。
這種思路的編碼結果就呈現爲下面的形式:
Key 1的編碼--Value 1長度指示的編碼--Value 1的編碼--Key 2的編碼--Value 2長度指示的編碼--Value 2的編碼--。。。。
能不能更加節省呢?PB更加高明之處就在於此。經過觀察能夠知道,在程序設計時,不少變量都是一個整數(int,int64等等),由於前面的編碼已經能夠對整數進行本身定界了,若是Value是整數,就無需長度指示了,豈不是浪費了?但不指示的話,怎麼知道後面是個整數呢?
PB因而把Key增長了3個比特(沒錯,就是3比特),記錄後面的Value的類型。Value的類型在PB中稱爲wire_type,用3比特表示。Key的形式就成爲:
(Key << 3) | wire_type
即將Key左移3位,最後3比特表示Value的類型。將這一整個東西用前面的方式編碼。
由於wire_type只有3比特,因此表示的信息是粗略的,主要有如下幾種:
wire type=0,表示這個Value是一個變長整數(用前面那種方式編碼),好比int32, int64, uint32, uint64, sint32, sint64, bool, enum;
wire type=1,表示這個Value是一個64位的數,好比fixed64, sfixed64, double,Value爲64位,8字節;(注意,int64的wire type=0,整數是變長編碼的)
wire type=2,表示string, bytes, embedded messages, packed repeated fields;(這些Value的長度須要在Key後面記錄下來)
wire type=3,表示groups中的Start Group,就是有一組,3表示接下來的Value是第一組;
wire type=4,表示groups中的End group;
wire type=5,表示32位固定長度的fixed32, sfixed32, float
好比,08 96 01這三個字節,由於第一個字節(08)的MSB=0,即:
0000 1000,去除MSB爲:0001000。
最後三位(000)表示wire type=0,說明後面的Value是一個Varint;
而前面的0001表示整數1,表示是編號爲第1個的變量;
後面的96 01,寫成二進制:
1001 0110 0000 0001
能夠看出,前一個字節的MSB=1,後一個字節的MSB=0,是完整的,去除掉兩個MSB:
0010110 0000001
由於低字節優先,因而串起來:00000010010110=150。
這樣,08 96 01這三個字節就表示第一個變量值爲整數150。
另外一個例子:12 07 74 65 73 74 69 6e 67
12的二進制爲0001 0010,由於MSB=0,因此是最後一個字節,去除MSB:0010010,後三位010表示wire type=2,前四位0010表示第2個變量。
由於wire type=2,表示Value是string, bytes等變長流。接下來的數記錄了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"}
其中,IntFlag和StringFlag假定是類的變量名,能夠看出,JSON使用了40個左右的字節,而PB使用了12個字節,若是這個對象被反覆傳遞(大多數程序通常都是這樣),則整體開銷很小。
至此,PB的格式基本已分析完畢。