什么是protobuf?
protobuf格式的特點
1. 效率高/性能好
對比XML這種文件傳輸格式,解析時間大大低於XML解析時間,同時空間上的開銷也減少了很多。
2. 可以很方便的生成文件
例如我們在進行文件傳輸時並不需要自己寫很多東西,僅僅定義一個proto文件即可生成所需的代碼。
3. 支持“向后兼容”和“向前兼容”
“向后兼容”(backward compatible),就是說,當模塊B升級了后,它能夠正確識別模塊A發出的老版本的協議。由於老版本沒有“狀態”這個屬性,在擴充協議時,可以考慮把“狀態”屬性設置成非必填 的,或者給“狀態”屬性設置一個缺省值即可。
“向前兼容”(forward compatible),就是說,當模塊A升級了之后, 模塊B能夠正常識別模塊A發出的新版本的協議。這時候,新增加的“狀態”屬性會被忽略。因此其可擴展性也很強。
4. 支持多種語言
Google官方發布的源代碼中包含了C++、Java、Python三種語言,這種天生就支持三種語言的格式簡直少見,下面小編會使用python來為大家進行簡單的講解。
資源網站大全 https://55wd.com 我的007辦公資源網站 https://www.wode007.com
Protobuf的使用
使用方法也比較簡單:
- 定義用於消息文件.proto
- 使用protobuf的編譯器編譯消息文件
- 使用編譯好對應語言的類文件進行消息的序列化與反序列化
先來定義一個簡單的消息:
message Person {
int32 id = 1;//24 string name = 2;//wujingchao string email = 3;//wujingchao92@gmail.com }
實際的二進制消息為:
08 18 12 0a 77 75 6a 69 6e 67 63 68 61 6f 1a 16 77 75 6a 69 6e 67 63 68 61 6f 39 32 40 67 6d 61 69 6c 2e 63 6f 6d
下面就講解這段二進制流數據是怎么組成的:
varints
一般情況下int類型都是固定4個字節,protobuf定義了一種變長的int,每個字節最高位表示后面還有沒有字節,低7位就為實際的值,並且使用小端的表示方法。例如1,varint的表示方法就為:
0000 0001
是不是這樣就省了三個字節。
再例如300,4字節表示為:10 0101100,varint表示為:
10101100 00000010
所以前面消息為Person的id的值為00011000,即0x18。
負數的最高位為1,如果負數也使用這種方式表示就會出現一個問題,int32總是需要5個字節,int64總是需要10個字節。
所以定義了另外一種類型:sint32,sint64。采用ZigZag編碼,所有的負數都使用正數表示,計算方式:
sint32 (n << 1) ^ (n >> 31) sint64 (n << 1) ^ (n >> 63)
Signed Original | Encoded As |
---|---|
0 | 0 |
-1 | 1 |
1 | 2 |
-2 | 3 |
2147483647 | 4294967294 |
-2147483648 | 4294967295 |
使用varint編碼的類型有int32, int64, uint32, uint64, sint32, sint64, bool, enum。Java里面沒有對應的無符號類型,int32與uint32一樣。
Wire Type
每個消息項前面都會有對應的tag,才能解析對應的數據類型,表示tag的數據類型也是varint。
tag的計算方式: (field_number << 3) | wire_type
每種數據類型都有對應的wire_type:
Wire 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 |
所以wire_type最多只能支持8種,目前有6種。
所以前面Person的id,field_number為1,wire_type為0,所以對應的tag為
1 <<< 3 | 0 = 0x08
Person的name,field_number為2,wire_type為2,所以對應的tag為
2 <<< 3 | 2 = 0x12
對應Length-delimited的wire type,后面緊跟着的varint類型表示數據的字節數。
所以name的tag后面緊跟的0x0a表示后面的數據長度為10個字節,即"wujingchao"的UTF-8 編碼或者ASCII值:
08 18 12 0a 77 75 6a 69 6e 67 63 68 61 6f 1a 16
嵌套的消息類型embedded messages與packed repeated fields也是使用這種方式表示,對應默認值的數據,是不會寫進protobuf消息里面的。
packed repeated與repeated的區別在於編碼方式不一樣,repeated將多個屬性類型與值分開存儲。而packed repeated采用Length-delimited方式。下面這個是官方文檔的例子:
message Test4 { repeated int32 d = 4 [packed=true]; } 22 // tag (field number 4, wire type 2) 06 // payload size (6 bytes) 03 // first element (varint 3) 8E 02 // second element (varint 270) 9E A7 05 // third element (varint 86942)
如果沒有packed的屬性是這樣存儲的:
20 //tag(field number 4,wire type 0) 03 //first element (varint 3) 20 //tag(field number 4,wire type 0) 8E 02//second element (varint 270) 20 //tag(field number 4,wire type 0) 9E A7 05 // third element (varint 86942)
是不是這種方式比較節省內存,所以proto3的repeated默認就是使用packed這種方式來存儲。(proto2與proto3區別在於.proto的語法)。