詳細(xì)分析protobuf(以下簡稱pb)數(shù)據(jù)序列化Tag-WireType-Value方式搜贤,對VARINT、帶符號整型的詳細(xì)分析彩倚,分別對
int32, int64, uint32, uint64, sint32, sint64, bool, enum滩援,fixed64, sfixed64, double, string, bytes, embedded messages, packed repeated fields, fixed32, sfixed32, float
所有protobuf支持的數(shù)據(jù)類型進(jìn)行說明破衔。通過demo和驗(yàn)證過程,相信能幫忙到大家理解protobuf的原理阳惹。
Tag-WireType-Value
pb里描述每一個(gè)數(shù)據(jù)對象均按這一規(guī)則谍失。
Tag是每個(gè)字段后的1、2莹汤、3 ……
-
WireType是指數(shù)據(jù)類型和Value的長度快鱼,下表列出全部類型:
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 Value是指字段實(shí)際值
下面的例子均按照proto3的協(xié)議定義來編寫,相比proto2纲岭,optional是默認(rèn)的抹竹。詳細(xì)請看官方文檔
syntax = "proto3";
package test;
option java_package = "com.example.test";
option java_outer_classname = "TestProtos";
message User {
int32 id = 1;
string name = 2;
repeated string icon_url = 3;
}
如果數(shù)據(jù)設(shè)置如下:
{
id : 10,
name : "Jo"
}
則編碼后的數(shù)據(jù)為080a12024a6f
hex | 說明 |
---|---|
08 | 這是Tag和WireType部分,規(guī)則為 $tag << 3 | WireType止潮。 tag:id(1)窃判,字段類型為VARINT(0),所以值為 1 << 3 | 0喇闸。 |
0a | 10的值袄琳。 |
12 | tag:id(2),字段類型為Length-delimited(2)燃乍,所以值為 2 << 3 | 2唆樊。 |
02 | 由于是字符串類型,這個(gè)段表示字符串的長度刻蟹,"Jo"長度為2逗旁。 |
4a | J的ASCII碼 |
6f | o的ASCII碼 |
可變長整形VARINT
一個(gè)int32來說,無論是1舆瘪,還是65535痢艺,都會占用4個(gè)字節(jié),而pb為了使數(shù)據(jù)更緊湊更節(jié)省空間介陶,使用了可
變長的int來表示數(shù)字堤舒。小的數(shù)用1個(gè)字節(jié)來表示,大的數(shù)用多個(gè)字節(jié)來表示哺呜。VARINT就是一種可使用1個(gè)或多個(gè)字節(jié)來表示整型的方法舌缤。
每個(gè)VARINT的最后一個(gè)字節(jié)是一個(gè)標(biāo)志位(msb),表示這個(gè)數(shù)字除了當(dāng)前字節(jié)是否有后一個(gè)字節(jié)來一起表示。
剩下的低7位表示數(shù)字的實(shí)際數(shù)值国撵。
例如數(shù)字1陵吸,用VARINT表示:
0000 0001
由于不需要后面的字節(jié)來表示,所以msb為0介牙,000 0001
表示1壮虫。
數(shù)字300,這個(gè)比較復(fù)雜一點(diǎn)环础,編碼后為08ac02
囚似,轉(zhuǎn)為二進(jìn)制:
1010 1100 0000 0010
分析:
1010 1100 0000 0010
→ 010 1100 000 0010 把msb去掉后
→ 000 0010 010 1100 pb使用的整形型是低位方式(little-endian),所以要把2個(gè)字節(jié)互換位置
→ 000 001 0010 1100 2個(gè)字節(jié)拼接在一起
→ 1 0010 1100 去掉前面的0
→ 256 + 32 + 8 + 4 = 300 2進(jìn)制轉(zhuǎn)換為10進(jìn)制
可見线得,編碼如1饶唤、300這樣值不高的數(shù)字時(shí),結(jié)果為1個(gè)字節(jié)到2個(gè)字節(jié)之間贯钩,對于我們大多數(shù)的實(shí)際場景來說是非常適合的募狂,能有效的節(jié)省編碼長度。
有符號整型
前面章節(jié)中看到角雷,所有VARINT(0)為0的都是用VARINT來表示的祸穷,包括的具體類型有int32, int64, uint32, uint64, sint32, sint64, bool, enum
。但是sint32/sint64
勺三,與int32/int64
在用于表示 負(fù)數(shù) 是有區(qū)別的粱哼。如果使用int32
表示一個(gè)負(fù)數(shù),結(jié)果會占用10個(gè)字節(jié)檩咱,實(shí)際上用了一個(gè)很大無符號數(shù)來表示揭措。如果使用sint32
,會使用ZigZag
編碼方式來提高效率刻蚯。
ZigZag
ZigZag
編碼是用一種正負(fù)數(shù)的交錯(cuò)方式來表達(dá)绊含,當(dāng)數(shù)值的絕對值小的時(shí)候,會比使用int32
的方式節(jié)省很多字節(jié)炊汹。例如-1編碼為1躬充,1編碼為2,-2編碼為3讨便,如下表:
帶符號數(shù) | 編碼后 |
---|---|
0 | 0 |
-1 | 1 |
1 | 2 |
-2 | 3 |
2147483647 | 4294967294 |
-2147483648 | 4294967295 |
例如:
message Signed {
int32 a = 1;
sint32 b = 2;
}
如果數(shù)據(jù)設(shè)置如下:
{
a : -10,
b : -10
}
則編碼后的數(shù)據(jù)為08f6ffffffffffffffff011013
hex | 說明 |
---|---|
08 | tag:id(1)充甚,int32字段類型為VARINT(0),所以值為 1 << 3 | 0霸褒。 |
f6ffffffffffffffff01 | 用一個(gè)很大的正數(shù)來表示的-10 |
10 | tag:id(2)伴找,sint32字段類型為VARINT(0),所以值為 2 << 3 | 0废菱。 |
13 | 用ZigZag表示的-10, 0x13 = 19技矮,符合上面編碼表推算出來的值 |
sint32
的值用這個(gè)公式表達(dá):(n << 1) ^ (n >> 31)
可見sint32
只用1個(gè)字節(jié)表示-10抖誉,int32
要用10個(gè)字節(jié)。
int32分析
接下來分析上面那串10個(gè)字節(jié)的int32:
內(nèi)容比較長衰倦,建議copy出來看
f6ffffffffffffffff01
→ 1111 0110 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 0000 0001 轉(zhuǎn)成2進(jìn)制
→ 111 0110 111 1111 111 1111 111 1111 111 1111 111 1111 111 1111 111 1111 111 1111 000 0001 把msb去掉后
→ 000 000 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 0110 把字節(jié)順序反轉(zhuǎn)袒炉,再拼在一起
→ ff ff ff ff ff ff ff f6 去掉前面的0,轉(zhuǎn)為16進(jìn)制樊零,這個(gè)數(shù)就是0xffffffffffffffff - 9 = -10(可以打印看Integer.toHexString(-10)的16進(jìn)制字符串)我磁。
非VARINT的數(shù)字
如上圖的字段類型表,float/fixed32/sfixed32
的字段類型為5驻襟,double/fixed64/sfixed64
的字段類型為1夺艰,它們使用固定長度方式來記錄數(shù)字,同樣也是使用little-endian字節(jié)順序塑悼,有符號類型也是使用ZigZag方式劲适。
enum枚舉
enum枚舉也是用VARINT來表示的楷掉,enum枚舉是帶默認(rèn)值的厢蒜,默認(rèn)為0的元素,默認(rèn)值是可以不編碼的烹植,因?yàn)榻獯a端默認(rèn)就是0斑鸦。空代表元素0草雕,01代表元素1巷屿,02代表元素2。
bool布爾值
bool布爾值也是用VARINT來表示的墩虹,bool是帶默認(rèn)值的嘱巾,默認(rèn)為false,默認(rèn)值是不編碼的诫钓,因?yàn)榻獯a端默認(rèn)就是false旬昭。空代表false菌湃,01代表true问拘。
string字符串
string字段類型為Length-delimited(2),最早的例子里出現(xiàn)過惧所,tag << 3 | 2骤坐,后面跟一個(gè)VARINT來表示字符串長度,緊跟著就是字符串的內(nèi)容下愈。
bytes二進(jìn)制數(shù)據(jù)
編碼方式跟string一致纽绍,但不會對字符串編碼進(jìn)行處理。轉(zhuǎn)換成Java類型為byte[]势似。
嵌套對象
一個(gè)類型嵌套另外一個(gè)類型:
message Test1 {
int32 a = 1;
}
message NestTest {
Test1 t = 1;
}
設(shè)置a的值為300顶岸,編碼后輸出為:0a0308ac02
腔彰。上面的例子已經(jīng)分析過300編碼后為08ac02
,對比可以看出辖佣,嵌套多出了前面0a03
霹抛。嵌套對象的字段類型為Length-delimited(2),因此 tag << 3 | 2 = 0a卷谈,后面跟著嵌套對象的長度杯拐,即08ac02
的長度為3,因此為03世蔗。
Repeated標(biāo)記可重復(fù)
在proto2里端逼,Repeated標(biāo)記的字段可以是0個(gè)或多個(gè)的數(shù)值,在Java里翻譯過來是array污淋。編碼后Repeated的數(shù)值可以是連續(xù)顶滩,也可以不連續(xù),因?yàn)槊看尉幋a都是按Tag-WireType-Value寸爆,最后把各個(gè)值merge合并礁鲁。
舉例:
message RepeatedTest {
repeated int32 a = 1;
}
數(shù)據(jù)為:1, 2, 3
編碼后:080108020803
0801 tag << 3 | 0 , 1
0802 tag << 3 | 0 , 2
0803 tag << 3 | 0 , 3
同一個(gè)tag出現(xiàn)多次,解碼的時(shí)候把多次進(jìn)行merge處理赁豆,還原出array結(jié)果仅醇。
Packed Repeated標(biāo)記為打包一塊可重復(fù)
表示把數(shù)據(jù)打包在一起形成一個(gè)類似于嵌套的形式,打包一起的數(shù)據(jù)不能分開魔种,必須連續(xù)析二。相比repeated少編碼了重復(fù)的tag。
舉例:
message RepeatedPackedTest {
repeated int32 a = 1 [packed=true];
}
數(shù)據(jù)為:1, 2, 3
編碼后:0a03010203
0a tag << 3 | 2 (Length-delimited(2))
03 payload节预,后面的數(shù)據(jù)長度
01 1
02 2
03 3
總結(jié)&建議
- proto2版本所有字段加上optional叶摄,proto3默認(rèn)為optional,不用手動(dòng)再添加安拟。
- 有負(fù)數(shù)的數(shù)值時(shí)使用
sint32/sint64
蛤吓。 - 正數(shù)沒超過2 ^ 28 - 1 = 268435455,則使用int32類型去扣,否則使用fixed32類型柱衔,當(dāng)然超過了2 ^ 32 - 1,就要使用
int64/fixed64
了愉棱。
源碼
至此把protobuf所有數(shù)據(jù)類型的原理進(jìn)行了分析唆铐,上面的示例代碼可到此下載:https://github.com/itvincent-git/protobuf-sample 。測試代碼在app/src/test/../ExampleUnitTest.java
奔滑。