Protocol Buffers編碼詳解,例子,圖解

Protocol Buffers編碼詳解,例子,圖解

本文不是讓你掌握protobuf的使用,而是以超級細緻的例子的方式分析protobuf的編碼設計。通過此文你可以瞭解protobuf的數據壓縮能力來自什麼地方,版本兼容如何做到的,其Key-Value編碼的設計思路。如果你詳細瞭解此文,你應該就能具備自己造一套編解碼輪子的能力(至少基本思路)。

測試的例子

閱讀圖片時請對比前面的例子和表格。每個字段的名稱都是包含了tag的。

複製代碼
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

函數

VALUEVARINT表示
VARINT
0

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 E1enum

11

0

E1_5

58 05

WriteEnumToArray

optional bool

12

0

true

60 01

WriteBoolToArray

VALUE固定4個字節
FIXED32
5

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個字節
FIXED64
1

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類的有長度的編碼
LENGTH_DELIMITED
2

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
WriteStringToArray

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
WriteVarint32ToArray
WriteInt32NoTagToArray

repeated string

23

2

"love","hate","C++"

ba 01 04 6c 6f 76 65 ba 01 0468 61 74 65 ba 01 03 43 2b 2b

VerifyUTF8StringNamedField
WriteStringToArray

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

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舉例:

VARINTS 類型

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

  ZigZag 有符號編碼

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 

 

4字節和8字節的固定長度編碼

 

double,float, 這些都是IEEE規定好的格式。大家反而都老實了。

fixed32,sfixed32,fixed64,fixed64,適合存放大數字數字。編碼後變成網絡序。

下圖是展示repeated fixed32的編碼,可以看到其實就是key重複出現。

變長LENGTH_DELIMITED

string,bytes

string的編碼還是key+value,只是value裏面多了一個長度。

string的要求是UTF8的編碼的。所以如果不是這個編碼最好用bytes。

string的編碼帶入沒有'\0'

下圖是repeated string

repeated VARINTS packed

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"}

 

 

Protobuf的解碼

 

編碼和解碼函數SerializeToArray,ParseFromArray,得到編碼size要調用函數ByteSize。如果要逃避required的IsInitialized()檢查檢查,可以用SerializePartialToArray, ParsePartialFromArray一類函數。當然後果自負。

proto的解碼就是找到key,根據key找到tag(代碼裏面叫fieldnumber),然後根據tag進行解碼,因爲編碼是KV的,編碼本事有一定的防錯性。

比較有意思的是google在代碼裏面會有預測下一個tag解碼處理。應該是爲了加速處理(不進入for循環)。

 

對比先驅們ASN.1,CDR等

 

其實拿protobuf和XML這類編碼比完全是不公平的較量,簡直就是欺負小P孩。真正應該拿來對比到時當年這些真正的編碼工具,比如電信中的ASN.1和CORBA中的CDR等。這些編碼先驅對數據的讀取操作往往是完全依靠生產的代碼(大部分沒有kv設計)。

我自己覺得最大改變來自當年的編解碼工具在編碼的時候,只着眼於雙方(比如異構系統)的數據值表示不一致時,將異構的數據編譯成數據流的問題,而protobuf在之上還解決了分佈系統中重要的麻煩,版本兼容的問題。

其實性能方面,這些先驅和Protobuf應該都在伯仲之間。

protobuf的數據類型支持其實並不豐富。但這樣也在多語言支持上輕鬆了很多。(想想給lua支持一個char,short),在編碼處理上也有很多化煩爲簡的設計。

 

protobuf的字段更新,版本兼容

 

KeyValue的編碼+可選項默認值方法保證了protobuf在版本兼容上有先天優勢。

關於字段更新,和版本兼容,google給出的建議:

【參考】https://developers.google.com/protocol-buffers/docs/proto ,

  • 不要試圖改變tag
  • 如果要新增一個field,請使用optionalrepeated,不要使用required。(高版本出現一個required,你當低編碼ó高解碼怎麼辦。)
  • 如果一個不需要的字段可以保留,避免其tag被勿用造成衝突,可以加入明顯的前綴OBSOLETE_標識。
  • int32, uint32, int64, uint64, and bool 是兼容的,他們都是使用VARINTS(v)進行編碼的。
  • sint32,sint64是兼容的他們都是使用VARINTS(Zigzag(v))進行編碼的。
  • 如果bytesUTF-8的編碼,那麼和string兼容
  • fixed32,sfixed32 兼容,fixed64sfixed64兼容。
  • optional 兼容repeated.(原文不夠準確,應該是如果沒有加pack=true修飾),爲什麼可以兼容?前面白畫那麼多圖了。

 

 

使用protobuf的建議

 

可以不使用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的偉大和挑戰

多語言支持是protobuf的重大加分項。其實這點社區貢獻良多。

KV的設計+可選和默認值,保證版本兼容。而且支持很天然,就我所知在其出來之前,沒有一個編碼工具把這個事情解決舒服了。

完善的文檔體系和開源的方式讓其獲得了大量擁戴,以及大量社區的支持。

我不爽protobuf的地方:

  • 不能和代碼直接交互使用,我更希望其能生成我代碼直接可以用的struct,另外部分是對這個struct的編碼函數。
  • request描述既然看起來那麼多餘,直接廢棄把。而可選可以直接通過默認值比較判定。
  • repeated是動態分配的。如果不重複使用,其實性能不高。我總覺得Flatbuffersbenchmark佔了這個便宜好像更多。
  • VARINTS的編碼有壓縮,但我總覺得爲了那幾個字節犧牲性能,不值得。如果字段少,減少不了幾個字節。如果字段多,也未見得多有效。我是做遊戲開發的,Who Care那幾個字節?(開始覺得google有點怪,他一搞文本處理的,Care幾個整數幹嘛?後面frost和wookin提醒我說google其實處理的是大量的docid,wordid,發現是自己腦子短路了。他確實有壓縮的必要。)
  • 好多代碼呀,好大的庫呀。
  • 反射的設計,反正你還是要寫不少代碼的。
  • 序列化爲啥不直接用Json,而要搞個類似Json

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加價一倍】

發佈了26 篇原創文章 · 獲贊 47 · 訪問量 59萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章