一胜蛉、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過程如下:
三、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ù)流如下:
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
5.2嵌套消息類型
嵌套消息類型采用T-L-V的存儲方式壳坪,外部消息的V即為嵌套消息的字段
舶得,在T-L-V的V中嵌套了一系列的T-L-V。
編碼方式:字段值(即V)根據(jù)字段的數(shù)據(jù)類型采用不同編碼方式爽蝴。
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
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存儲多次江解。
為了解決Tag數(shù)據(jù)冗余,采用帶packed=true的repeated字段存儲方式徙歼,即將相同的Tag只存儲一次犁河、添加repeated字段下所有字段值的長度Length、連續(xù)存儲repeated字段值魄梯,組成一個大的Tag - Length - Value -Value -Value對桨螺,即T - L - V - V - V對。
通過采用帶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序列化原理解析