Protobuf序列化原理

一胜蛉、Protobuf序列化原理簡介

1.1序列化

序列化是將數(shù)據(jù)結(jié)構(gòu)或?qū)ο筠D(zhuǎn)換成二進制字節(jié)流的過程瓷炮。
Protobuf對于不同的字段類型采用不同的編碼方式和數(shù)據(jù)存儲方式對消息字段進行序列化圆凰,以確保得到高效緊湊的數(shù)據(jù)壓縮婶恼。
Protobuf序列化過程如下:
(1)判斷每個字段是否有設(shè)置值燃乍,有值才進行編碼楞卡。
(2)根據(jù)字段標(biāo)識號與數(shù)據(jù)類型將字段值通過不同的編碼方式進行編碼霜运。
(3)將編碼后的數(shù)據(jù)塊按照字段類型采用不同的數(shù)據(jù)存儲方式封裝成二進制數(shù)據(jù)流脾歇。

1.2反序列化

反序列化是將在序列化過程中所生成的二進制字節(jié)流轉(zhuǎn)換成數(shù)據(jù)結(jié)構(gòu)或者對象的過程。
Protobuf反序列化過程如下:
(1)調(diào)用消息類的parseFrom(input)解析從輸入流讀入的二進制字節(jié)數(shù)據(jù)流淘捡。
(2)將解析出來的數(shù)據(jù)按照指定的格式讀取到C++藕各、Java、Phyton對應(yīng)的結(jié)構(gòu)類型中焦除。

二激况、Protobuf編碼方式

2.1Varint編碼

Varint編碼是一種變長的編碼方式,編碼原理是用字節(jié)表示數(shù)字膘魄,值越小的數(shù)字乌逐,使用越少的字節(jié)數(shù)表示。因此创葡,可以通過減少表示數(shù)字的字節(jié)數(shù)進行數(shù)據(jù)壓縮黔帕。
對int32類型的數(shù)字,一般需要4個字節(jié)表示蹈丸。如果采用Varint編碼成黄,對于很小的int32類型數(shù)字,則可以用1個字節(jié)來表示逻杖;雖然大的數(shù)字會需要5個字節(jié)來表示奋岁,但大多數(shù)情況下,消息都不會有很大的數(shù)字荸百,所以采用Varint編碼方式總是可以用更少的字節(jié)數(shù)來表示數(shù)字闻伶。
Varint編碼后每個字節(jié)的最高位都有特殊含義:
A、如果是1够话,表示后續(xù)的字節(jié)也是數(shù)字的一部分蓝翰。
B、如果是0女嘲,表示本字節(jié)是最后一個字節(jié)畜份,且剩余7位都用來表示數(shù)字。
當(dāng)使用Varint解碼時時欣尼,只要讀取到最高位為0的字節(jié)時爆雹,表示本字節(jié)是一個值經(jīng)Varint編碼后得到的字節(jié)流的最后一個字節(jié)。
在計算機內(nèi)愕鼓,負數(shù)一般會被表示為很大的整數(shù) 钙态,因為計算機定義負數(shù)的符號位為數(shù)字的最高位,如果采用Varint編碼方式表示一個負數(shù)菇晃,那么一定需要5個byte(因為負數(shù)的最高位是1册倒,會被當(dāng)做很大的整數(shù)處理)
Protobuf定義了sint32 / sint64類型表示負數(shù),通過先采用Zigzag編碼(將有符號數(shù)轉(zhuǎn)換成無符號數(shù))磺送,再采用Varint編碼驻子,從而用于減少編碼后的字節(jié)數(shù)灿意。
對于一個int32類型的值300的Varint編碼如下:
300的二進制編碼為:100101100(256+32+8+4)
從字節(jié)流末尾取出7bit并在最高位增加1構(gòu)成一個字節(jié):[1]010 1100
從字節(jié)流末尾取出7bit并在最高位增加1構(gòu)成一個字節(jié),如果是最后一個字節(jié)增加0:[0]0000010
兩字節(jié)為:[0]0000010 [1]010 1100
轉(zhuǎn)換為小端模式:10101100 00000010
編碼結(jié)果:1010 1100 0000 0010

2.2Zigzag編碼

Zigazg編碼是一種變長的編碼方式拴孤,其編碼原理是使用無符號數(shù)來表示有符號數(shù)字脾歧,使得絕對值小的數(shù)字都可以采用較少字節(jié)來表示,特別對表示負數(shù)的數(shù)據(jù)能更好地進行數(shù)據(jù)壓縮演熟。
Zigzag編碼對Varint編碼在表示負數(shù)時不足的補充鞭执,從而更好的幫助Protobuf進行數(shù)據(jù)的壓縮。因此芒粹,如果提前預(yù)知字段值是可能取負數(shù)的時候兄纺,需要采用sint32/sint64數(shù)據(jù)類型。
Protobuf通過Varint和Zigzag編碼后化漆,大大減少了字段值占用字節(jié)數(shù)估脆。
-2的Zigzag過程如下:


gRPC快速入門(二)——Protobuf序列化原理解析

三、Protobuf數(shù)據(jù)存儲方式

3.1T-L-V數(shù)據(jù)存儲方式

T-L-V(Tag - Length - Value)座云,即標(biāo)識符-長度-字段值的存儲方式疙赠,其原理是以標(biāo)識符-長度-字段值表示單個數(shù)據(jù),最終將所有數(shù)據(jù)拼接成一個字節(jié)流朦拖,從而實現(xiàn)數(shù)據(jù)存儲的功能圃阳。
其中Length可選存儲,如儲存Varint編碼數(shù)據(jù)就不需要存儲Length璧帝,此時為T-V存儲方式捍岳。

T-L-V 存儲方式的優(yōu)點:
A、不需要分隔符就能分隔開字段睬隶,減少了分隔符的使用锣夹。
B、各字段存儲得非常緊湊苏潜,存儲空間利用率非常高银萍。
C、如果某個字段沒有被設(shè)置字段值窖贤,那么該字段在序列化時的數(shù)據(jù)中是完全不存在的砖顷,即不需要編碼,相應(yīng)字段在解碼時才會被設(shè)置為默認值赃梧。

3.2T-V數(shù)據(jù)存儲方式

消息字段的標(biāo)識號、數(shù)據(jù)類型豌熄、字段值經(jīng)過Protobuf采用Varint和Zigzag編碼后授嘀,以T-V(Tag-Value)方式進行數(shù)據(jù)存儲。
對于Varint與Zigzag編碼方式編碼的數(shù)據(jù)锣险,省略了T-L-V中的字節(jié)長度Length蹄皱。

Tag是消息字段標(biāo)識符和數(shù)據(jù)類型經(jīng)Varint與Zigzag編碼后的值览闰,因此Tag存儲了字段的標(biāo)識符(field_number)和數(shù)據(jù)類型(wire_type),即Tag = 字段數(shù)據(jù)類型(wire_type) + 標(biāo)識號(field_number)巷折。
Tag占用一個字節(jié)的長度(如果標(biāo)識符大于15压鉴,則占用多一個字節(jié)的位置),字段數(shù)據(jù)類型(wire_type)占用3個bit锻拘,字段標(biāo)識符(field_number)占用4個bit油吭,最高位用于Varint編碼保留。

Tag = (field_number << 3) | wire_type
enum WireType { 
      WIRETYPE_VARINT = 0, 
      WIRETYPE_FIXED64 = 1, 
      WIRETYPE_LENGTH_DELIMITED = 2, 
      WIRETYPE_START_GROUP = 3, 
      WIRETYPE_END_GROUP = 4, 
      WIRETYPE_FIXED32 = 5
   };

解碼時署拟,Protobuf根據(jù)Tag將Value對應(yīng)于消息中的字段婉宰。

message person
{ 
   required int32     id = 1;  
   // wire type = 0,field_number =1 
   required string    name = 2;  
   // wire type = 2推穷,field_number =2 
 }

對于Person消息的name字段的Tag編碼如下:

nameTag = 2 << 3 | 2
nameTag = 0001 0010

根據(jù)Tag解碼得到filed_number心包、wire_type:

nameTag = 0001 0010
field_number = nameTag >> 3
field_number = 0010
wire_type = nameTag & 3
wire_type = 010

四、Protobuf序列化原理解析

Protobuf對于數(shù)據(jù)存儲的三大原則:
(1)Protocol Buffer將消息中的每個字段進行編碼后馒铃,利用T - L - V 存儲方式進行數(shù)據(jù)的存儲蟹腾,最終得到一個二進制字節(jié)流。
(2)ProtoBuf對于不同數(shù)據(jù)類型采用不同的序列化方式(數(shù)據(jù)編碼方式與數(shù)據(jù)存儲方式)
Protobuf對于不同的字段類型采用不同的編碼和數(shù)據(jù)存儲方式對消息字段進行序列化区宇,以確保得到高效緊湊的數(shù)據(jù)壓縮娃殖。不同類型的數(shù)據(jù)采用的編碼方式和存儲方式如下:


wire_type
只有六種類型,所以用三位二進制數(shù)完全足夠表示萧锉。
Tag = (field_number << 3) | wire_type

對于Varint編碼數(shù)據(jù)的存儲珊随,不需要存儲字節(jié)長度Length,使用T-V存儲方式進行存儲柿隙;對于采用其它編碼方式(如LENGTH_DELIMITED)的數(shù)據(jù)叶洞,使用T-L-V存儲方式進行存儲。
(3)ProtoBuf對于數(shù)據(jù)字段值的獨特編碼方式與T-L-V數(shù)據(jù)存儲方式禀崖,使得 ProtoBuf序列化后數(shù)據(jù)量體積極小衩辟。

2、WireType=0的序列化
WireType=0的類型包括int32波附,int64艺晴,uint32,unint64掸屡,bool封寞,enum以及sint32和sint64。
編碼方式采用Varint編碼(如果為負數(shù)仅财,采用Zigzag輔助編碼)狈究,數(shù)據(jù)存儲方式使用T-V方式存儲二進制字節(jié)流。

3盏求、WireType=1的序列化
WireType=1的類型包括fixed64抖锥,sfixed64亿眠,double。
編碼方式采用64bit編碼(編碼后數(shù)據(jù)大小為64bit磅废,高位在后纳像,低位在前),數(shù)據(jù)存儲方式使用T-V方式存儲二進制字節(jié)流拯勉。

4竟趾、WireType=2的序列化
WireType=2的類型包括string,bytes谜喊,嵌套消息潭兽,packed repeated字段。
對于編碼方式斗遏,標(biāo)識符Tag采用Varint編碼山卦,字節(jié)長度Length采用Varint編碼,string類型字段值采用UTF-8編碼诵次,嵌套消息類型的字段值根據(jù)嵌套消息內(nèi)部的字段數(shù)據(jù)類型進行選擇账蓉,
數(shù)據(jù)存儲方式使用T-L-V方式存儲二進制字節(jié)流。

5逾一、WireType=5的序列化
WireType=5的類型包括fixed32铸本,sfixed32,float遵堵。
編碼方式采用32bit編碼(編碼后數(shù)據(jù)大小為32bit箱玷,高位在后,低位在前)陌宿,數(shù)據(jù)存儲方式使用T-V方式存儲二進制字節(jié)流锡足。

五Protobuf序列化示例

5.1String類型

String類型字段的值使用UTF-8編碼。消息數(shù)據(jù)流如下:


gRPC快速入門(二)——Protobuf序列化原理解析
message Test
{
    required string str = 2;
}

// 將str設(shè)置為:testing
Test.setStr(“testing”)

// 經(jīng)過protobuf編碼序列化后的數(shù)據(jù)以二進制的方式輸出
// 輸出為:18, 7, 116, 101, 115, 116, 105, 110, 103
gRPC快速入門(二)——Protobuf序列化原理解析
5.2嵌套消息類型

嵌套消息類型采用T-L-V的存儲方式壳坪,外部消息的V即為嵌套消息的字段
舶得,在T-L-V的V中嵌套了一系列的T-L-V。
編碼方式:字段值(即V)根據(jù)字段的數(shù)據(jù)類型采用不同編碼方式爽蝴。


gRPC快速入門(二)——Protobuf序列化原理解析
message Test2
{
    required string str = 1;
    required int32 id1 = 2贡未;
}

message Test3 {
  required Test2 c = 1;
}

// 將Test2中的字段str設(shè)置為:testing
// 將Test2中的字段id1設(shè)置為:296
// 編碼后的字節(jié)為:10 淹魄,12 ,18思灌,7约啊,116, 101, 115, 116, 105, 110, 103栋荸,16挺据,-88吉嫩,2
gRPC快速入門(二)——Protobuf序列化原理解析
5.3通過packed修飾的 repeat 字段
message Test
{
    repeated int32 Car = 4 ;
    // 表達方式1:不帶packed=true
    repeated int32 Car = 4 [packed=true];
    // 表達方式2:帶packed=true
}

Test.setCar(3);
Test.setCar(270)缤底;
Test.setCar(86942)顾患;

如果序列化時對多個 T - V對存儲(不帶packed=true),則會導(dǎo)致Tag的冗余个唧,即相同的Tag存儲多次江解。


gRPC快速入門(二)——Protobuf序列化原理解析

為了解決Tag數(shù)據(jù)冗余,采用帶packed=true的repeated字段存儲方式徙歼,即將相同的Tag只存儲一次犁河、添加repeated字段下所有字段值的長度Length、連續(xù)存儲repeated字段值魄梯,組成一個大的Tag - Length - Value -Value -Value對桨螺,即T - L - V - V - V對。


gRPC快速入門(二)——Protobuf序列化原理解析

通過采用帶packed=true 的 repeated字段存儲方式酿秸,從而更好地壓縮序列化后的數(shù)據(jù)長度灭翔。

六Protobuf使用建議

基于Protobuf序列化原理分析,為了有效降低序列化后數(shù)據(jù)量的大小辣苏,可以采用以下措施:
(1)多用 optional或 repeated修飾符
若optional 或 repeated 字段沒有被設(shè)置字段值肝箱,那么該字段在序列化時的數(shù)據(jù)中是完全不存在的,即不需要進行編碼稀蟋,但相應(yīng)的字段在解碼時會被設(shè)置為默認值煌张。
(2)字段標(biāo)識號(Field_Number)盡量只使用1-15,且不要跳動使用
Tag是需要占字節(jié)空間的退客。如果Field_Number>16時骏融,F(xiàn)ield_Number的編碼就會占用2個字節(jié),那么Tag在編碼時就會占用更多的字節(jié)萌狂;如果將字段標(biāo)識號定義為連續(xù)遞增的數(shù)值档玻,將獲得更好的編碼和解碼性能。
(3)若需要使用的字段值出現(xiàn)負數(shù)粥脚,請使用sint32/sint64窃肠,不要使用int32/int64。
采用sint32/sint64數(shù)據(jù)類型表示負數(shù)時刷允,會先采用Zigzag編碼再采用Varint編碼冤留,從而更加有效壓縮數(shù)據(jù)。
(4)對于repeated字段树灶,盡量增加packed=true修飾
增加packed=true修飾纤怒,repeated字段會采用連續(xù)數(shù)據(jù)存儲方式,即T - L - V - V -V方式天通。

轉(zhuǎn)載文章
Protocol Buffer 序列化原理大揭秘 - 為什么Protocol Buffer性能這么好泊窘?
gRPC快速入門(二)——Protobuf序列化原理解析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子烘豹,更是在濱河造成了極大的恐慌瓜贾,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件携悯,死亡現(xiàn)場離奇詭異祭芦,居然都是意外死亡,警方通過查閱死者的電腦和手機憔鬼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門龟劲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人轴或,你說我怎么就攤上這事昌跌。” “怎么了照雁?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵蚕愤,是天一觀的道長。 經(jīng)常有香客問我囊榜,道長审胸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任卸勺,我火速辦了婚禮砂沛,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘曙求。我一直安慰自己碍庵,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布悟狱。 她就那樣靜靜地躺著静浴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪挤渐。 梳的紋絲不亂的頭發(fā)上苹享,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天,我揣著相機與錄音浴麻,去河邊找鬼得问。 笑死,一個胖子當(dāng)著我的面吹牛软免,可吹牛的內(nèi)容都是我干的宫纬。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼膏萧,長吁一口氣:“原來是場噩夢啊……” “哼漓骚!你這毒婦竟也來了蝌衔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蝌蹂,失蹤者是張志新(化名)和其女友劉穎噩斟,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體叉信,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡亩冬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了硼身。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡覆享,死狀恐怖佳遂,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情撒顿,我是刑警寧澤丑罪,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站凤壁,受9級特大地震影響吩屹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜拧抖,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一煤搜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧唧席,春花似錦擦盾、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至徒仓,卻和暖如春腐碱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背掉弛。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工症见, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人狰晚。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓筒饰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親壁晒。 傳聞我的和親對象是個殘疾皇子瓷们,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,060評論 2 355