原文轉自:http://www.wuzesheng.com/?p=1258
本文的主要內容是google protobuf中序列化數據時用到的編碼規則,但是,介紹具體的編碼規則之前,我覺得有必要先簡單介紹一下google protobuf。因此,本文首先會介紹一些google protobuf相關的內容,讓讀者朋友對google protobuf有一個初步的印象,然后,再開始進入正題—-深入淺出地介紹google protobuf中用到的編碼規則。下面言歸正傳,開始今天的話題。
1. Google-ProtoBuf是什么
ProtoBuf,全稱是Protocol Buffers, 它是谷歌內部用的一種高效的、可擴展的對結構化數據進行編碼的格式規范。谷歌自己內部很多程序之間的通信協議都用了ProtoBuf。
ProtoBuf可以支持多種編程語言,目前已經C++, Java和Python,本文中所前的內容用到例子的話,會以C++為例。
2.如何得到Google-ProtoBuf
ProtoBuf在Google Code上的主頁是:http://code.google.com/p/protobuf/, 感興趣的朋友可以在這里下載ProtoBuf的源碼,也可以在這里閱讀ProtoBuf的詳細的文檔。
3. 深入淺出Google-ProtoBuf中的編碼規則
(1)序列化和反序列化:
在開始本部分的內容之前,首先有必要介紹兩個基本概念,一個是序列化,一個是反序列化。這兩個概念的定義在網上搜一下都很多的,但大多都講得比較晦澀,不太好理解,在這里我會用比較通俗的文字來解釋,盡可能讓讀都朋友們一讀就明白是怎么回事:
序列化:是指將結構化的數據按一定的編碼規范轉成指定格式的過程
反序列化:是指將轉成指定格式的數據解析成原始的結構化數據的過程
舉個例子,Person是一個表示人的對象類型,person是一個Person類型的對象,將person存到一個對應的XML文檔中的過程就是一種序列化,而解析XML生成對應Person類型對象person的過程,就是一個反序列化的過程。在這里結構化數據指的就是Person類型的數據,一定的編碼規范指的就是XML文檔的規范。XML是一種簡單的序列化方式,用XML序列化的好處是,XML的通用性比較好,另外,XML是一種文本格式,對人閱讀比較友好,但是XML方式比較占空間,效率也不是很高。通常,比較高效的序列化都是采用二進制方式的,將要序列化的結構化數據,按一定的編碼規范,轉成為一串二進制的字節流存儲下來,需要用的時候再從這串二進制的字節流中反序列化出對應的結構化的數據。
通過上面的介紹,我們給protobuf下一個比較正式的定義了:Google ProtoBuf是Google制定的一種用來序列化結構化數據的程序庫。
(2)ProtoBuf中的編碼:
1) ProtoBuf編碼基礎——Varints, varints是一種將一個整數序列化為一個或者多個Bytes的方法,越小的整數,使用的Bytes越少。
Varints的基本規則是:
(a) 每個Byte的最高位(msb)是標志位,如果該位為1,表示該Byte后面還有其它Byte,如果該位為0,表示該Byte是最后一個Byte。
(b)每個Byte的低7位是用來存數值的位
(c)Varints方法用Litte-Endian(小端)字節序
舉個例子:300用Varints序列化的結果是1010 1100 0000 0010,運算過程如下 所示:
1010 1100 0000 0010->010 1100 000 0010(去標志位)->
000 0010 010 1100(調整字節序)-> 1 0010 1100 ->256+32+8+4=300(計算值)
2)ProtoBuf中消息的編碼規則:
(a)每條消息(message)都是有一系列的key-value對組成的, key和value分別采用不同的編碼方式。
(b)對某一條件消息(message)進行編碼的時候,是把該消息中所有的key-value對序列化成二進制字節流;而解碼的時候,解碼程序讀入二進制的字節流,解析出每一個key-value對,如果解碼過程中遇到識別不出來的類型,直接跳過。這樣的機制,保證了即使該消息添加了新的字段,也不會影響舊的編/解碼程序正常工作。
(c)key由兩部分組成,一部分是在定義消息時對字段的編號(field_num),另一部分是字段類型(wire_type)。字段類型定義如下表所示。
| Type | Meaning | Used For |
|---|---|---|
| 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 | Start group | groups (deprecated) |
| 4 | End group | groups (deprecated) |
| 5 | 32-bit | fixed32, sfixed32, float |
(d)key的編碼方式:field_num << 3 | wire_type
(e)varint類型(wire_type=0)的編碼,與第(1)部分中介紹的方法基本一致,但是int32, int64和sint32,sint64有些特別之處:int32和int64就是簡單的按varints方法來編碼,所以像-1、-2這樣負數也會占比較多的Bytes。於是sint32和sint64采用了一種改進的方法:先采用Zigzag方法將所有的整數(正數、0和負數)一一映射到所有的無符號數上,然后再采用varints編碼方法進行編碼。Zigzag映射函數為:
Zigzag(n) = (n << 1) ^ (n >> 31), n為sint32時
Zigzag(n) = (n << 1) ^ (n >> 63), n為sint64時
下表是一個比較直觀的映射表,這樣映射后再進行編碼的好處就是絕對值比較小的負數序列化后的結果占的Bytes數也會比較少。
| Signed Original | Encoded As |
|---|---|
| 0 | 0 |
| -1 | 1 |
| 1 | 2 |
| -2 | 3 |
| 2 | 4 |
| -3 | 5 |
| … | … |
| 2147483647 | 4294967294 |
| -2147483648 | 4294967295 |
(f)64-bit(wire_type=1)和32-bit(wire_type=5)的編碼方式就比較簡單了,直接在key后面跟上64bits或32bits,采用Little-Endian(小端)字節序。
(g)length-delimited(wire_type=2)的編碼方式:key+length+content, key的編碼方式是統一的,length采用varints編碼方式,content就是由length指定的長度的Bytes。
(h)wire_type=3和4的現在已經不推薦使用了,因此這里也不再做介紹。
3)ProtoBuf編解碼中字段順序(Field order)的問題:
(a) 編碼/解碼與字段順序無關,這一點由key-value機制就能保證
(b)對於未知的字段,編碼的時候會把它寫在序列化完的已知字段后面。
