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