Protobuf原理分析

image.png

詳細(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奔滑。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末艾岂,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子朋其,更是在濱河造成了極大的恐慌王浴,老刑警劉巖脆炎,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異氓辣,居然都是意外死亡秒裕,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門钞啸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來几蜻,“玉大人,你說我怎么就攤上這事体斩∷笾桑” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵絮吵,是天一觀的道長弧烤。 經(jīng)常有香客問我,道長蹬敲,這世上最難降的妖魔是什么暇昂? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮粱栖,結(jié)果婚禮上话浇,老公的妹妹穿的比我還像新娘脏毯。我一直安慰自己闹究,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布食店。 她就那樣靜靜地躺著渣淤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吉嫩。 梳的紋絲不亂的頭發(fā)上价认,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機(jī)與錄音自娩,去河邊找鬼用踩。 笑死,一個(gè)胖子當(dāng)著我的面吹牛忙迁,可吹牛的內(nèi)容都是我干的脐彩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼姊扔,長吁一口氣:“原來是場噩夢啊……” “哼惠奸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起恰梢,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤佛南,失蹤者是張志新(化名)和其女友劉穎梗掰,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嗅回,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡及穗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了绵载。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拥坛。...
    茶點(diǎn)故事閱讀 40,090評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖尘分,靈堂內(nèi)的尸體忽然破棺而出猜惋,到底是詐尸還是另有隱情,我是刑警寧澤培愁,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布著摔,位于F島的核電站,受9級特大地震影響定续,放射性物質(zhì)發(fā)生泄漏谍咆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一私股、第九天 我趴在偏房一處隱蔽的房頂上張望摹察。 院中可真熱鬧,春花似錦倡鲸、人聲如沸供嚎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽克滴。三九已至,卻和暖如春优床,著一層夾襖步出監(jiān)牢的瞬間劝赔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工胆敞, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留着帽,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓移层,卻偏偏與公主長得像仍翰,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子幽钢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容