ProtoBuf的使用以及原理分析
Protocal Buffers(簡(jiǎn)稱protobuf)是Google的一項(xiàng)技術(shù),用于結(jié)構(gòu)化的數(shù)據(jù)序列化、反序列化。
Protobuf的使用比較廣泛,常用于RPC 系統(tǒng)(Remote Procedure Call Protocol System)和持續(xù)數(shù)據(jù)存儲(chǔ)系統(tǒng)。其主要優(yōu)點(diǎn)是空間開銷小和性能比較好,類似于XML生成和解析,但protobuf的效率高于XML,不過protobuf生成的是字節(jié)碼,可讀性比XML差。
相關(guān)官方文檔:
Protocol Buffers官網(wǎng):https://developers.google.com/protocol-buffers/
Protocol Buffers官網(wǎng)(中文):https://developers.google.com/protocol-buffers/?hl=zh-CN
Git地址:https://github.com/google/protobuf
Java API:https://developers.google.com/protocol-buffers/docs/reference/java/?hl=zh-CN
proro文件的編寫指南:https://developers.google.com/protocol-buffers/docs/style?hl=zh-CN
Java使用指南:https://developers.google.com/protocol-buffers/docs/javatutorial?hl=zh-CN
一.protobuf的基本應(yīng)用
使用protobuf開發(fā)的基本步驟為:
?1.配置開發(fā)環(huán)境 安裝protocol 代碼編譯器
?2.編寫對(duì)應(yīng)的.proto文件,定義序列化對(duì)象結(jié)構(gòu)
?3.使用編譯器生成對(duì)應(yīng)的序列化工具類
?4.編寫自己的應(yīng)用
使用github上的protoc-3.5.1-win32.zip?https://github.com/google/protobuf/releases
每個(gè)Protobuf 的 字段 都有一定的格式:
限定修飾符?| 數(shù)據(jù)類型 | 字段名稱?| = | 字段編碼值?| [字段默認(rèn)值]
1.限定修飾符:?required/optional/repeated
Required:表示是一個(gè)必須字段,缺失該字段會(huì)引發(fā)編解碼異常,導(dǎo)致消息被丟棄。
Optional:表示是一個(gè)可選字段。
Repeated:表示該字段可重復(fù),每次可以包含0~N個(gè)值,表示集合
2.數(shù)據(jù)類型
protobuf定義了一些基本的數(shù)據(jù)類型
string/bytes/bool/int32/int64/float/double
enum 枚舉類? ? ?message 自定義類
3.字段名稱
字段名稱的命名方式與C、Java等語言的變量命名方式幾乎是相同的。
protobuf建議字段的命名采用以下劃線分割的駝峰式。例如 建議使用first_name 而不是firstName.
4.字段編碼值
編碼值的取值范圍為 1~2^32(4294967296)。
相同的編碼值,其限定修飾符和數(shù)據(jù)類型必須相同。
消息中的字段的編碼值無需連續(xù),只要是合法的,并且不能在同一個(gè)消息中有字段包含相同的編碼值。不過通常習(xí)慣使用連續(xù)的字段編碼值,比較易于理解
⑤.默認(rèn)值。
在發(fā)送數(shù)據(jù)時(shí),對(duì)于required數(shù)據(jù)類型,如果用戶沒有設(shè)置值,則使用默認(rèn)值傳遞。當(dāng)接受數(shù)據(jù)時(shí),對(duì)于optional字段,如果沒有接收到對(duì)應(yīng)值,則設(shè)置為默認(rèn)值
以下是一個(gè)簡(jiǎn)單的User對(duì)象的proto文件
syntax="proto2";option java_package = "com.chenpp.serializer.protobuf"; option java_outer_classname="UserProto"; message User {//1,2表示當(dāng)前序列化后的字段順序required int32 age = 2; //年齡required string name = 1;//姓名}使用proto自己的編譯器進(jìn)行編譯,生成實(shí)體類
protoc.exe --java_out=./java? ?./user.proto?
–java_out 后面是生成java文件存放地址?
最后的參數(shù)是proto文件的名稱,可以寫絕對(duì)地址,也可以直接寫proto文件名稱
引入protobuf對(duì)應(yīng)的dependency,打印對(duì)應(yīng)的序列化結(jié)果:
<dependency><groupId>com.google.protobuf</groupId><artifactId>protobuf-java</artifactId><version>3.5.0</version></dependency> public class ProtoSerializer implements ISerializer {public <T> byte[] serialize(T obj) {UserProto.User user = UserProto.User.newBuilder().setAge(21).setName("chenpp").build();return user.toByteArray();}public <T> T deserialize(byte[] data, Class<T> clazz) {try {UserProto.User user = UserProto.User.parseFrom(data);return (T) user;} catch (InvalidProtocolBufferException e) {e.printStackTrace();}return null;} }==================protobuf======================== protobuf {age:21}序列化后的byte[]的length: 10 10 6 99 104 101 110 112 112 16 21 name: "chenpp" age: 21二.protobuf序列化的原理
之前那篇文章,講過Json里的序列化結(jié)果為: { "name":"chenpp","age":21}? -- 一共26個(gè)字節(jié),而想要將其進(jìn)行進(jìn)一步壓縮,就需要去掉一些冗余的字節(jié)
?思路:1)能不能去掉定義屬性(約定1=name,2=age)? 約定了字段,約定了類型? 去除分隔符(引號(hào),冒號(hào),逗號(hào)之類的)
? ? ? ? ?2)壓縮數(shù)字,因?yàn)槿粘=?jīng)常使用到的都是一些比較小的數(shù)字,一共int占4個(gè)字節(jié),但實(shí)際有效的字節(jié)數(shù)沒有那么多
? ? ? ? ??把英文轉(zhuǎn)化成數(shù)字(ASCII),并對(duì)數(shù)字進(jìn)行壓縮
protobuf里使用到了兩種壓縮算法:varint和zigzag算法
varint算法
這是針對(duì)無符號(hào)整數(shù),一種壓縮方式
壓縮方法:
1.對(duì)一個(gè)無符號(hào)整數(shù),將其換算成二進(jìn)制
2.從右往左取7位,然后在最高位補(bǔ)1
3.一直繼續(xù),直到取到最后一個(gè)有意義的字節(jié)(在最后一個(gè)有意義的字節(jié)上最高位補(bǔ)0)
4.先取到的字節(jié)排在后取到的字節(jié)前面,得到一個(gè)新的字節(jié),轉(zhuǎn)換成十進(jìn)制就是壓縮的結(jié)果
以:500為例:其實(shí)有意義的就是2個(gè)字節(jié)
?0000?0001 1111? 0100= 2^2+2^4+2^5+2^6+2^7+2^8 = 4+16+32+64+128+256 = 500
按照其壓縮方式得到的新的二進(jìn)制字節(jié)為:
1111?0100? | 0000 0011
1111?0100 代表的是負(fù)數(shù),使用補(bǔ)碼(正數(shù)取反+1),并且最高位符號(hào)位為1
轉(zhuǎn)化后: 先?-1為 1111?0011? ?取反? 0000 1100 = 12
計(jì)算出來就是 -12 3
字符如何轉(zhuǎn)化成數(shù)字編碼
對(duì)于英文字母,這里的name字段,使用ASCII碼對(duì)照表查找對(duì)應(yīng)的數(shù)字
chenpp: 對(duì)應(yīng)的ASCII碼
c-99 ?h-104? e-101 ?n-110? p-112
按照varint的算法 取7位補(bǔ)最高位為1(最后一個(gè)字節(jié)最高位補(bǔ)0)
對(duì)于小于127(2^7-1=127)的數(shù)字,其有效字節(jié)只有1位,壓縮的時(shí)候最高位補(bǔ)0,故壓縮之前和壓縮之后的數(shù)字沒有變化
protobuf的存儲(chǔ)格式
protobuf采用T-L-V的格式進(jìn)行存儲(chǔ)
[Tag | length | value ]
l其中l(wèi)ength為可選, 但是string必須有l(wèi)ength(這樣在反序列化的時(shí)候程序才知道該字符串從哪里開始到哪里結(jié)束), 而int是不需要length的
Tag:字段標(biāo)識(shí)符,用于標(biāo)識(shí)字段 其值等于
??? field_number(當(dāng)前字段的編號(hào),第幾個(gè)字段)<<3|wire_type(int64/int32/可變長(zhǎng)度string)
Length:Value的字節(jié)長(zhǎng)度 (string需要有,int不需要)
Value:消息字段經(jīng)過編碼后的值
Age:int32? 2<<3|0 = 16
Name: string 1<<3|2 = 10?
在反序列化的時(shí)候根據(jù)tag?mod 8 的余數(shù)判斷對(duì)應(yīng)的wireType,從而知道該字段對(duì)應(yīng)的存儲(chǔ)方式和編碼方式
對(duì)應(yīng)User(name="chenpp",age=21)按照varint進(jìn)行壓縮后其序列化結(jié)果為:
c-99 ?h-104? e-101 ?n-110? p-112
String:tag-length-value?
Int32:tag-value
10??????? 6????????? 99????? 104????? 101??? 110?? 112?? 112????? 16???????? 21
Tag – length? -? c? -???? h???? –?? e??? -?? n?? -?? p?? -? p?? -? Tag -? Value
根據(jù)上述壓縮算法可知:對(duì)于int32/int64,value有且只有最后一個(gè)字節(jié)為正數(shù),故當(dāng)遇到第一個(gè)為正數(shù)的字節(jié)時(shí)就知道其value值已經(jīng)獲取完畢,所以對(duì)于int32類型的字段,不需要length,只需要tag和value就足夠了
ZigZag算法
在計(jì)算機(jī)中,負(fù)數(shù)會(huì)被表示為很大的整數(shù),因?yàn)樨?fù)數(shù)的符號(hào)位在最高位,如果使用varint算法進(jìn)行壓縮的話會(huì)需要 32/7 ~ 5個(gè)字節(jié),反
而加大了空間的開銷.故在protobuf中對(duì)于有符號(hào)整數(shù)會(huì)使用sint32/sint64來表示。protobuf中負(fù)數(shù)的壓縮方式是先使用ZigZag算把有符號(hào)數(shù)(無論數(shù)值是正數(shù)還是負(fù)數(shù),都會(huì)進(jìn)行一次壓縮計(jì)算)轉(zhuǎn)化為無符號(hào)數(shù),再使用varint進(jìn)行壓縮
ZigZag算法的思路:
負(fù)數(shù)之所以不好壓縮:一個(gè)原因是因?yàn)槠渥罡呶粸?,另一個(gè)原因是對(duì)于絕對(duì)值比較小的負(fù)數(shù),其正數(shù)會(huì)有很多的前導(dǎo)零,那么在使用補(bǔ)碼表示負(fù)數(shù)的時(shí)候(取反+1),會(huì)導(dǎo)致負(fù)數(shù)會(huì)有很多的前導(dǎo)1,使得無法壓縮
所以ZigZag采用的辦法就是:先將符號(hào)位從最高位移動(dòng)到最低位,其余數(shù)字均往前移動(dòng)1位;然后再對(duì)所有的數(shù)字(符號(hào)位除外)進(jìn)行取反,這樣得到的計(jì)算結(jié)果就是一個(gè)可以壓縮的數(shù)字(符號(hào)位不占據(jù)最高位,而小絕對(duì)值的數(shù)值由于取反操作其前導(dǎo)1都變?yōu)榱饲皩?dǎo)0)
比方說-300:
其對(duì)應(yīng)的正數(shù)的原碼為: 0000 0000 0000 0000 0000 0001 0010 1100
取反: 1111 1111 1111 1111 1111 1110 1101 0011
再+1:??? 1111 1111 1111 1111??1111 1110 1101 0100 (-300)
移動(dòng)符號(hào)位之后: 1111 1111 1111 1111 1111 1101 1010 1001
取反:0000 0000 0000 0000 0000 0010 0101 0111
計(jì)算后為0010 0101 0111 = 599
在ZigZag算法里也是使用這種思路對(duì)有符號(hào)整數(shù)進(jìn)行壓縮的,將其轉(zhuǎn)化成表達(dá)式就是
Sint32: (n<<1)^ (n>>31)
Sint64: (n<<1)^(n>>63)
當(dāng)n為正數(shù)時(shí),n>>31為0000 0000 0000 0000 0000 0000 0000 0000;當(dāng)n為負(fù)數(shù)時(shí),n>>31為1111 1111 1111 1111 1111 1111 1111 1111
(n>>31)與(n<<1)進(jìn)行異或之后,如果n為正數(shù),(n>>31)^(n<<1) =n<<1;如果n為負(fù)數(shù),其計(jì)算結(jié)果和上述所說的最高位移動(dòng)到最后,然后取反效果是一樣的(n<<1補(bǔ)的最低位為0和n>>31異或運(yùn)算之后一定為1,而其他位上與1做異或運(yùn)算相當(dāng)于取反),這樣一來就可以使用varint進(jìn)行壓縮計(jì)算了
對(duì)-300的ZigZag計(jì)算結(jié)果:599?使用varint算法進(jìn)行壓縮
得到1101 0111?? 0000? 0100
其結(jié)果為:-41?? 4
到此,protobuf的壓縮原理就介紹完了
總結(jié)
以上是生活随笔為你收集整理的ProtoBuf的使用以及原理分析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Apollo分布式配置中心踩坑
- 下一篇: 滑动窗口算法学习(一)