Protobuf使用規範分享
一、Protobuf 的優點
Protobuf 有如 XML,不過它更小、更快、也更簡單。它以高效的二進制方式存儲,比 XML 小 3 到 10 倍,快 20 到 100 倍。你可以定義自己的數據結構,然後使用代碼生成器生成的代碼來讀寫這個數據結構。你甚至可以在無需重新部署程序的情況下更新數據結構。只需使用 Protobuf 對數據結構進行一次描述,即可利用各種不同語言或從各種不同數據流中對你的結構化數據輕松讀寫。
有兩項技術保證了采用 Protobuf 的程序能獲得相對於 XML 極大的性能提高。
第一點,我們可以考察 Protobuf 序列化後的信息內容。您可以看到 Protocol Buffer 信息的表示非常緊湊,這意味著消息的體積減少,自然需要更少的資源。比如網絡上傳輸的字節數更少,需要的 IO 更少等,從而提高性能。
第二點,我們需要理解 Protobuf 封解包的大致過程,從而理解為什麽會比 XML 快很多。詳細看以下鏈接 http://www.ibm.com/developerworks/cn/linux/l-cn-gpb/
另外,它有一個非常棒的特性,即“向後”兼容性好,人們不必破壞已部署的、依靠“老”數據格式的程序就可以對數據結構進行升級。這樣您的程序就可以不必擔心因為消息結構的改變而造成的大規模的代碼重構或者遷移的問題。因為添加新的消息中的 field 並不會引起已經發布的程序的任何改變。
Protobuf 語義更清晰,無需類似 XML 解析器的東西(因為 Protobuf 編譯器會將 .proto 文件編譯生成對應的數據訪問類以對 Protobuf 數據進行序列化、反序列化操作)。 使用 Protobuf 無需學習復雜的文檔對象模型,Protobuf 的編程模式比較友好,簡單易學,同時它擁有良好的文檔和示例,對於喜歡簡單事物的人們而言,Protobuf 比其他的技術更加有吸引力。
二、Protobuf消息定義
消息由至少一個字段組合而成,類似於C語言中的結構。每個字段都有一定的格式。
字段格式:限定修飾符① | 數據類型② | 字段名稱③ | = | 字段編碼值④ | 字段默認值⑤
1)限定修飾符包含 required\optional\repeated
Required: 表示是一個必須字段,必須相對於發送方,在發送消息之前必須設置該字段的值,對於接收方,必須能夠識別該字段的意思。發送之前沒有設置required字段或者無法識別required字段都會引發編解碼異常,導致消息被丟棄。至於為什麽感興趣的自己可以到protobuf官網深入研究其編解碼原理。http://code.google.com/p/protobuf/
Optional:表示是一個可選字段,可選對於發送方,在發送消息時,可以有選擇性的設置或者不設置該字段的值。對於接收方,如果能夠識別可選字段就進行相應的處理,如果無法識別,則忽略該字段,消息中的其它字段正常處理。---因為optional字段的特性,很多接口在升級版本中都把後來添加的字段都統一的設置為optional字段,這樣老的版本無需升級程序也可以正常的與新的軟件進行通信,只不過新的字段無法識別而已,因為並不是每個節點都需要新的功能,因此可以做到按需升級和平滑過渡。
Repeated:表示該字段可以包含0,N個元素。其特性和optional一樣,但是每一次可以包含多個值。可以看作是在傳遞一個數組的值。
2)數據類型
Protobuf定義了一套基本數據類型。幾乎都可以映射到C\C++\Java等語言的基礎數據類型.
另外,有一點特意強調一下:
關於 fixed32 和int32的區別。fixed32的打包效率比int32的效率高,但是使用的空間一般比int32多。因此一個屬於時間效率高,一個屬於空間效率高。根據項目的實際情況,一般選擇fixed32,如果遇到對傳輸數據量要求比較苛刻的環境,可以選擇int32.
3)字段編碼值
有了該值,通信雙方才能互相識別對方的字段。當然相同的編碼值,其限定修飾符和數據類型必須相同.
編碼值的取值範圍為 1~2^32(4294967296)。其中 1~15的編碼時間和空間效率都是最高的,編碼值越大,其編碼的時間和空間效率就越低(相對於1-15),當然一般情況下相鄰的2個值編碼效率的是相同的,除非2個值恰好實在4字節,12字節,20字節等的臨界區。比如15和16.
有一點需要強調,消息中的字段的編碼值無需連續,只要是合法的,並且不能在同一個消息中有字段包含相同的編碼值。
三、protobuf編解碼原理
1) ProtoBuf編碼基礎——Varints, varints是一種將一個整數序列化為一個或者多個Bytes的方法,越小的整數,使用的Bytes越少。
Varints的基本規則是:
(a) 每個Byte的最高位(msb)是標誌位,如果該位為1,表示該Byte後面還有其它Byte,如果該位為0,表示該Byte是最後一個Byte。
(b)每個Byte的低7位是用來存數值的位
(c)Varints方法用Litte-Endian(小端)字節序
2)ProtoBuf中消息的編碼規則:
(a)每條消息(message)都是有一系列的key-value對組成的, key和value分別采用不同的編碼方式。
(b)對某一條件消息(message)進行編碼的時候,是把該消息中所有的key-value對序列化成二進制字節流;而解碼的時候,解碼程序讀入二進制的字節流,解析出每一個key-value對,如果解碼過程中遇到識別不出來的類型,直接跳過。這樣的機制,保證了即使該消息添加了新的字段,也不會影響舊的編/解碼程序正常工作。
(c)key由兩部分組成,一部分是字段編碼值(field_num),另一部分是字段類型(wire_type)。key = field_num << 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 | Start group | groups (deprecated) |
4 | End group | groups (deprecated) |
5 | 32-bit | fixed32, sfixed32, float |
(d)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時
(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的現在已經不推薦使用了,因此這裏也不再做介紹。
如果希望更深入的理解它,可以從代碼上面去進一步研究,主要接口有protobuf_c_message_unpack、protobuf_c_message_pack、protobuf_c_message_free_unpacked等,路徑/wns/commonlibs/3party/protobuf-c-0.15/protobuf-c-0.15-low-memory-version/src/google/protobuf-c
實際使用過程當中,常常出現一些使用不當的情況導致了程序異常,下面列舉常見的幾個情況:
1、新增字段限定修飾符設置為required(項目投入運營以後涉及到版本升級時的新增消息字段如果使用了required,需要全網統一升級)
2、多個版本同時在開發時,往同一個結構裏邊加入相同字段編碼值的新optional字段(要知道字段編碼值正是處於這種兼容性的考慮)
第2種情況會導致版本兼容性問題,代碼示例如下:
發送端(AP),發送函數:
void send_person_info_to_wac() { Wns__PersonInfo person = WNS__PERSON_INFO__INIT; person.name = "sanzer"; person.gender = 1; person.has_option = 1; person.option = 5; wns_ipc_to_local_direct(WNS__MODID, 0, WNS__MSGID, (ProtobufCMessage *)&person); }
proto定義:
package wns; person_info{ requried string name = 1; required int32 gender = 2; optioned int32 option = 3; }
接收端(WAC),接收函數:
int32_t show_person_info_cb(const void *buf, int32_t len, const ProtobufCMessage *msg, const struct wns_cmd_ipc_hdr_t *proxy_hdr) { assert(msg); Wns__PersonInfo *person = (Wns__PersonInfo *)msg; return 0; } // 註冊回調函數: wns_ipc_reg_callback(WNS__MSGID, show_person_info_cb, &wns__person_info__descriptor, AUTO_FREE);
proto定義:
package wns; person_info{ require string name = 1; require int32 gender = 2; optioned double option = 3; }
分別編譯兩種proto,並將動態庫拷貝到指定目錄。在接收端(wac)和發送端(ap)分別運行各自程序,然後觀察接收端解析的數據格式。
結果證明接收端與發送端option字段,都采用了相同的字段編碼值(3),但是不同的數據屬性(發送端:int32,接收端:double),這種情況下,接收端將會解析失敗。
四、protobuf使用規範
為了更好的使用它,現制定以下規範:
1、不要修改已經存在的字段編碼值
2、新增字段必須為optional或repeated,否則無法保證新老程序在互相傳遞消息時的消息兼容性。
3、在原有的消息中,不能移除已經存在的required字段,optional和repeated類型的字段可以被移除,但是他們之前使用的標簽號必須被保留,不能被新的字段重用。
4、新增字段標簽號可以不連續但不能重復。
Protobuf使用規範分享