Protocol Buffers編碼詳解,例子,圖解html
本文不是讓你掌握protobuf的使用,而是以超級細緻的例子的方式分析protobuf的編碼設計。經過此文你能夠了解protobuf的數據壓縮能力來自什麼地方,版本兼容如何作到的,其Key-Value編碼的設計思路。若是你詳細瞭解此文,你應該就能具有本身造一套編解碼輪子的能力(至少基本思路)。git
閱讀圖片時請對比前面的例子和表格。每一個字段的名稱都是包含了tag的。github
message S2 { optional int32 s2_1 = 1; optional string s2_2 = 2 ; } enum E1 { E1_1 = 1; E1_3 = 3; E1_5 = 5; } message S3 { optional int32 s3_1 = 1; //設置爲0x88 optional int32 s3_2 = 2; //設置爲0x8888 optional uint32 s3_3 = 3; //設置爲0xE8E8E8 optional uint32 s3_4 = 4; //設置爲0xE8E8E8E8 optional int64 s3_5 = 5; //設置爲0x8888 optional int64 s3_6 = 6; //設置爲0xE8E8E8E8 optional uint64 s3_7 = 7; //設置爲0xE8E8E8E8 optional uint64 s3_8 = 8; //設置爲0xE8E8E8E8E8E8E8E8 optional sint32 s3_9 = 9; //設置爲0x8888 optional sint32 s3_10 = 10; //設置爲-0x8888 optional sint64 s3_64 = 64; //注意這個tag id 設置爲0xE8E8E8E8 optional sint64 s3_65 = 65; //注意這個tag id 設置爲-0xE8E8E8E8 optional E1 s3_11 = 11; //設置爲E1_5 optional bool s3_12 = 12; //設置爲true optional float s3_13 = 13; //設置 float,設置爲88.888 optional fixed32 s3_14 = 14; //設置爲 0x8888 optional sfixed32 s3_15 = 15; //設置爲 -0x8888 optional double s3_16 = 16; //設置 double,設置爲8888.8888 optional fixed64 s3_17 = 17; //設置爲 0x8888888888 optional sfixed64 s3_18 = 18; //設置爲 -0x8888888888 optional string s3_19 = 19; //設置爲 "I love you,C++!" optional bytes s3_20 = 20; //設置爲 "I hate you,C++!" repeated int32 s3_21 = 21; //設置爲3, 270, and 86942, 用google文檔的例子 repeated int32 s3_22 = 22 [packed = true]; //設置爲3, 270, and 86942 repeated string s3_23 = 23; //設置爲"love","hate","C++" optional S2 s3_24 = 24; //設置爲 0x1,"love" repeated S2 s3_25 = 25; //設置爲 0x16,"love" and 0x16,"hate" repeated fixed32 s3_26 = 26; //設置爲1,2,3 optional int32 s3_27 = 27; //不設置 }
編碼的的數據表格以下,後面的剖析都會依賴這個表格進行。算法
分類說明數組 |
定義網絡 |
TAG函數 |
WriteType工具 |
設置的值性能 |
編碼後的16進制數據 KEY+(LENGTH)+VLAUE測試 |
函數 |
VALUE用VARINT表示 |
optional int32 |
1 |
0 |
0x88 |
08 88 01 |
WriteInt32ToArray |
optional int32 |
2 |
0 |
0x8888 |
10 88 91 02 |
WriteInt32ToArray |
|
optional uint32 |
3 |
0 |
0xE8E8E8 |
18 e8 d1 a3 07 |
WriteUInt32ToArray |
|
optional uint32 |
4 |
0 |
0xE8E8E8E8 |
20 e8 d1 a3 c7 0e |
WriteUInt32ToArray |
|
optional int64 |
5 |
0 |
0x8888 |
28 88 91 02 |
WriteInt64ToArray |
|
optional int64 |
6 |
0 |
0xE8E8E8E8 |
30 e8 d1 a3 c7 0e |
WriteInt64ToArray |
|
optional uint64 |
7 |
0 |
0xE8E8E8E8 |
38 e8 d1 a3 c7 0e |
WriteUInt64ToArray |
|
optional uint64 |
8 |
0 |
0xE8E8E8E8E8E8E8E8 |
40 e8 d1 a3 c7 8e 9d ba f4 e8 01 |
WriteUInt64ToArray |
|
optional sint32 |
9 |
0 |
0x8888 |
48 90 a2 04 |
WriteSInt32ToArray |
|
optional sint32 |
10 |
0 |
-0x8888 |
50 8f a2 04 |
WriteSInt32ToArray |
|
optional E1(enum) |
11 |
0 |
E1_5 |
58 05 |
WriteEnumToArray |
|
optional bool |
12 |
0 |
true |
60 01 |
WriteBoolToArray |
|
VALUE固定4個字節 |
optional float |
13 |
5 |
88.888 |
6d a8 c6 b1 42 |
WriteFloatToArray |
optional fixed32 |
14 |
5 |
0x8888 |
75 88 88 00 00 |
WriteFixed32ToArray |
|
optional sfixed32 |
15 |
5 |
-0x8888 |
7d 78 77 ff ff |
WriteSFixed32ToArray |
|
VALUE固定8個字節 |
optional double |
16 |
1 |
8888.8888 |
81 01 58 ca 32 c4 71 5c c1 40 |
WriteDoubleToArray |
optional fixed64 |
17 |
1 |
0x8888888888 |
89 01 88 88 88 88 88 00 00 00 |
WriteFixed64ToArray |
|
optional sfixed64 |
18 |
1 |
-0x8888888888 |
91 01 78 77 77 77 77 ff ff ff |
WriteSFixed64ToArray |
|
repeated,message,string,btyes類的有長度的編碼 |
optional string |
19 |
2 |
"I love you,C++!" |
9a 01 0f 49 20 6c 6f 76 65 20 79 6f 75 2c 43 2b 2b 21 |
VerifyUTF8StringNamedField |
optional bytes |
20 |
2 |
"I hate you,C++!" |
a2 01 0f 49 20 68 61 74 65 20 79 6f 75 2c 43 2b 2b 21 |
WriteBytesToArray |
|
repeated int32 (對比) |
21 |
0 |
3,270,86942 |
a8 01 03 a8 01 8e 02 a8 01 9e a7 05 |
WriteInt32ToArray |
|
repeated int32 [packed=true] |
22 |
2 |
3,270,86942 |
b2 01 06 03 8e 02 9e a7 05 |
WriteTagToArray |
|
repeated string |
23 |
2 |
"love","hate","C++" |
ba 01 04 6c 6f 76 65 ba 01 04 68 61 74 65 ba 01 03 43 2b 2b |
VerifyUTF8StringNamedField |
|
optional S2(message) |
24 |
2 |
{0x1,"love"} |
c2 01 08 08 01 12 04 6c 6f 76 65 |
WriteMessageNoVirtualToArray |
|
repeated S2 |
25 |
2 |
S2{0x16,"love"} ,S2{0x16,"hate"} |
ca 01 08 08 16 12 04 6c 6f 76 65 ca 01 08 08 16 12 04 68 61 74 65 |
WriteMessageNoVirtualToArray |
|
repeated fixed32(對比) |
26 |
5 |
1,2,3 |
d5 01 01 00 00 00 d5 01 02 00 00 00 d5 01 03 00 00 00 |
WriteFixed32ToArray |
|
可選沒有設置 |
optional int32 |
27 |
0 |
沒有設置 |
沒有數據 |
|
數據是安裝tag排序進行編碼的 |
optional sint64 |
64 |
0 |
0xE8E8E8E8 |
80 04 90 a2 04 |
WriteSInt64ToArray |
optional sint64 |
65 |
0 |
-0xE8E8E8E8 |
88 04 8f a2 04 |
WriteSInt64ToArray |
message中的fields按照tag順序進行編碼,而每一個fields的採用key+value的方式保存編碼數據。若是一個optional,或者repeated的fields沒有被設置,那麼他在編碼的數據中徹底不存在。相應的字段在解碼的時候回設置爲默認值。若是一個required的標識的fields沒有被設置,那麼在IsInitialized()檢查會失敗。編碼的順序和元數據.proto文件內fields的定義數據無關,而是根據tag的從小到大的順序進行的編碼。
key-value的設計保證了protobuf的版本兼容。高<->低,和低<->高均可以適配。(若是高版本編碼增長了required 字段,低版本數據解碼後會認爲IsInitialized() 失敗,因此慎用required )
protobuf的總體數據都是變長的,並且有必定的自描述能力,因此其設計的核心點就是能識別出每個key,value,(length)。
先要說明,protobuf編碼對本身的類型進行了再歸類,其歸類類型就是WireType
Type, |
枚舉定義,WireType |
Meaning |
對應的protobuf類型 |
編碼長度 |
0 |
WIRETYPE_VARINT |
Varint |
int32, int64, uint32, uint64, sint32, sint64, bool, enum |
變長,1-10個字節,用VARINT編碼且壓縮 |
1 |
WIRETYPE_FIXED64 |
64-bit |
fixed64, sfixed64, double |
固定8個字節 |
2 |
WIRETYPE_LENGTH_DELIMITED |
Length-delimited |
string, bytes, embedded messages, packed repeated fields |
變長,在key後會跟一個長度定義 |
3 |
WIRETYPE_START_GROUP |
Start group |
groups (deprecated) |
已經要廢棄了,不看也罷 |
4 |
WIRETYPE_END_GROUP |
End group |
groups (deprecated) |
|
5 |
WIRETYPE_FIXED32 |
32-bit |
fixed32, sfixed32, float |
固定4個字節 |
KEY = VARINT(fields_tag<<3|WireType)
fields_tag就是元數據描述.proto文件裏面的tag。
WireType他們就是這個field類型對應的WireType的枚舉值。見前面定義表中定義。
生產的數據再用VARINT(後面介紹)進行編碼。
當類型VARINT整數數組 (好比repeated int32 ),若是不加packed=true修飾時,key=VARINT(fields_tag<<3|WriteType :0),視WireType爲VARINT ,若是加上packed=true修飾時,仍然KEY = VARINT(fields_tag<<3|WireType:2),視類型爲LENGTH_DELIMITED。
用字段s3_17的key舉例:
Base 128 bits VARINS
前面說過變長編碼的最大挑戰是要找到每一個字段邊界。因此就必須能用方法能在編碼的數據裏面找到這個數值。
用連續字節的msb(most significant bit)爲1,表示後續的字節仍然是這個數字。當首msb爲0,表示結束。這個方法在UTF編碼裏面也經常使用。
例子,紅色的bit都是表示連續,藍色bit表示結束。
源: 0x8888 1000 1000 1000 1000
編: 0x029188 0000 0010 1001 0001 1000 1000
源:0xE8E8E8 1110 1000 1110 1000 1110 1000
編:0x07A3D1E8 0000 0111 1010 0011 1101 0001 1110 1000
KEY,LENGTH的編碼也是用VARINTS
s3_2的字段例子
s3_3的走低am的;ozone
VARINS大部分時候均可以壓縮數值,但若是數值很大時,反而會增長一些消耗,好比int64極限0xFFFFFFFFFFFFFFFF下須要10個字節,因此一看就有一個弱點, VARINS若是直接使用對於有符號數值不利。
因此google對此增長sint32,sint64類型,其會先採用ZigZag編碼,而後再VARINS ,不說廢話了,直接上google的表格示例:
算法(L我看了示例也沒有想到能這樣寫)
(n << 1) ^ (n >> 31)
(n << 1) ^ (n >> 63)
Signed Original |
Encoded As |
0 |
0 |
-1 |
1 |
1 |
2 |
-2 |
3 |
2147483647 |
4294967294 |
-2147483648 |
4294967295 |
double,float, 這些都是IEEE規定好的格式。你們反而都老實了。
fixed32,sfixed32,fixed64,fixed64,適合存放大數字數字。編碼後變成網絡序。
下圖是展現repeated fixed32的編碼,能夠看到其實就是key重複出現。
string的編碼仍是key+value,只是value裏面多了一個長度。
string的要求是UTF8的編碼的。因此若是不是這個編碼最好用bytes。
string的編碼帶入沒有'\0'
下圖是repeated string
repeated 的VARINTS 有帶packed=true 時也是變長,帶packed=true的描述會壓縮更多,但和普通repeated模式不太同樣。
下面的例子是帶有packed的字段s3_22的例子
下面是不帶packe=true的例子。
內嵌類,中間潛入類S2的例子,s3_24{1,"love"},內嵌類裏面的編碼方式和外部同樣,只是內嵌類的tag使用其本身的tag。
下面的例子是repeated S2 s3_25{22,"love"},{22,"hate"}
編碼和解碼函數SerializeToArray,ParseFromArray,獲得編碼size要調用函數ByteSize。若是要逃避required的IsInitialized()檢查檢查,能夠用SerializePartialToArray, ParsePartialFromArray一類函數。固然後果自負。
proto的解碼就是找到key,根據key找到tag(代碼裏面叫fieldnumber),而後根據tag進行解碼,由於編碼是KV的,編碼本事有必定的防錯性。
比較有意思的是google在代碼裏面會有預測下一個tag解碼處理。應該是爲了加速處理(不進入for循環)。
其實拿protobuf和XML這類編碼比徹底是不公平的較量,簡直就是欺負小P孩。真正應該拿來對比到時當年這些真正的編碼工具,好比電信中的ASN.1和CORBA中的CDR等。這些編碼先驅對數據的讀取操做每每是徹底依靠生產的代碼(大部分沒有kv設計)。
我本身以爲最大改變來自當年的編解碼工具在編碼的時候,只着眼於雙方(好比異構系統)的數據值表示不一致時,將異構的數據編譯成數據流的問題,而protobuf在之上還解決了分佈系統中重要的麻煩,版本兼容的問題。
其實性能方面,這些先驅和Protobuf應該都在伯仲之間。
protobuf的數據類型支持其實並不豐富。但這樣也在多語言支持上輕鬆了不少。(想一想給lua支持一個char,short),在編碼處理上也有不少化煩爲簡的設計。
KeyValue的編碼+可選項默認值方法保證了protobuf在版本兼容上有先天優點。
關於字段更新,和版本兼容,google給出的建議:
【參考】https://developers.google.com/protocol-buffers/docs/proto ,
能夠不使用requested,只是用optional+default 默認值, requested只是將你須要作的檢查交給了protobuf。代價是版本兼容的麻煩。不如不用。
版本兼容寶典:字段只新增,不刪除,字段描述不用requested,任什麼時候候tag不要變更,類型變化要慎重只有兼容才能夠(但仍是慎重把!),optional到repeated的變換也能夠(只要沒用pack=true)。
tag是要佔空間的,若是tag>16時,KEY的編碼就會佔用2個字節了。因此tag的定義儘可能不要跳動。
若是要出現負數,不要使用int32,int64,而應該使用sint32,sint64。
string真要UTF8的,有檢查的。
repeated的VARINTS類型,能夠增長packed=true減少佔用空間,但有低版本不兼容的風險。
對於repeated字段的使用,protobuf是有提早(預分配)分配空間的,擴展基本就是乘以2,對同一個message,若是已經分配好空間了,Clear並不回收這一空間。因此儘可能使用一個控制點編解碼比較好。
.proto生成的message對應的結構很重,在遊戲開發中不合適直接使用。須要你本身的數據和message的結構之間轉換。(這個我很不爽)
多語言支持是protobuf的重大加分項。其實這點社區貢獻良多。
KV的設計+可選和默認值,保證版本兼容。並且支持很自然,就我所知在其出來以前,沒有一個編碼工具把這個事情解決舒服了。
完善的文檔體系和開源的方式讓其得到了大量擁戴,以及大量社區的支持。
我不爽protobuf的地方:
google的還有一個新的爲遊戲開發準備的編碼方式Flatbuffers,人家還在不斷進步。
《google的protobuf文檔》https://developers.google.com/protocol-buffers/docs/overview
《google的protobuf編碼解釋》https://developers.google.com/protocol-buffers/docs/encoding
《Flatbuffer的bechmark》http://google.github.io/flatbuffers/md__benchmarks.html
《protobuf詳解》http://www.cnblogs.com/cobbliu/archive/2013/03/02/2940074.html 很是詳細的一篇文章,惟一感受沒有說清的地方是可變長度的字段。
ferguszhang(張峯禎)的protobuf介紹圖片。
【本文做者是雁渡寒潭,本着自由的精神,你能夠在無盈利的狀況完整轉載此文檔,轉載時請附上BLOG連接:http://www.cnblogs.com/fullsail/,不然每字一元,每圖一百不講價。對Baidu文庫和360doc加價一倍】