Protobuf的簡單介紹、使用和分析 一、protobuf是什麼? protobuf(Google Protocol Buffers)是Google提供一個具有高效的協議數據交換格式工具庫(類似Json),但相比於Json,Protobuf有更高的轉化效率,時間效率和空間效率都是JSON的3-5倍 ...
Protobuf的簡單介紹、使用和分析
一、protobuf是什麼?
protobuf(Google Protocol Buffers)是Google提供一個具有高效的協議數據交換格式工具庫(類似Json),但相比於Json,Protobuf有更高的轉化效率,時間效率和空間效率都是JSON的3-5倍。後面將會有簡單的demo對於這兩種格式的數據轉化效率的對比。但這個庫目前使用還不是太流行,據說谷歌內部很多產品都有使用。
二、protobuf有什麼?
Protobuf 提供了C++、java、python語言的支持,提供了windows(proto.exe)和linux平臺動態編譯生成proto文件對應的源文件。proto文件定義了協議數據中的實體結構(message ,field)
關鍵字message: 代表了實體結構,由多個消息欄位(field)組成。
消息欄位(field): 包括數據類型、欄位名、欄位規則、欄位唯一標識、預設值
數據類型:常見的原子類型都支持(在FieldDescriptor::kTypeToName中有定義)
欄位規則:(在FieldDescriptor::kLabelToName中定義)
required:必須初始化欄位,如果沒有賦值,在數據序列化時會拋出異常
optional:可選欄位,可以不必初始化。
repeated:數據可以重覆(相當於java 中的Array或List)
欄位唯一標識:序列化和反序列化將會使用到。
預設值:在定義消息欄位時可以給出預設值。
三、protobuf有什麼用?
Xml、Json是目前常用的數據交換格式,它們直接使用欄位名稱維護序列化後類實例中欄位與數據之間的映射關係,一般用字元串的形式保存在序列化後的位元組流中。消息和消息的定義相對獨立,可讀性較好。但序列化後的數據位元組很大,序列化和反序列化的時間較長,數據傳輸效率不高。
Protobuf和Xml、Json序列化的方式不同,採用了二進位位元組的序列化方式,用欄位索引和欄位類型通過演算法計算得到欄位之前的關係映射,從而達到更高的時間效率和空間效率,特別適合對數據大小和傳輸速率比較敏感的場合使用。
四、Protobuf在Android上的使用
1、創建proto文件,定義消息的實體結構
2、編譯proto文件生成對應的java文件
3、添加protobuf-java-2.5.0.jar到android工程
4、在android中實現對消息結構的序列化/反序列化
五、Protobuf與json的對比
1、創建product.proto文件
定義了三個Message(ProductInfo、PhoneInfo、Watch)消息結構
2、消息結構對應的java類(ProductInfo、PhoneInfo、Watch)
3、消息結構和java對象賦值
PhoneName:” idol3”
Price:2000
Top:1
WatchName:” tcl watch”
Price:1000
Top:1
4、JSON字元串
{"phone":{"phoneName":"idol3","price":2000,"top":1},"watch":{"watchName":"tcl wtch","top":1,"price":1000}}
5、Protobuf轉化後的二進位文件
空間效率
Json:107個位元組
Protobuf:32個位元組
時間效率
Json序列化: 1ms , 反序列化:0ms
Protobuf 序列化: 0ms 反序列化:0ms
將public List<Phone> list和repeated PhoneInfo phoneInfoList =3;都賦值為1000個PhoneInfo
空間效率
Json:4206個位元組
Protobuf:1332個位元組
時間效率
Json序列化: 4ms , 反序列化:1ms
Protobuf 序列化: 1ms 反序列化:0ms
六、protobuf的簡單分析
1、優缺點
優點:通過以上的時間效率和空間效率,可以看出protobuf的空間效率是JSON的2-5倍,時間效率要高,對於數據大小敏感,傳輸效率高的模塊可以採用protobuf庫
缺點:消息結構可讀性不高,序列化後的位元組序列為二進位序列不能簡單的分析有效性;目前使用不廣泛,只支持java,C++和Python;
2、數據序列化/反序列化
a、規則:
protobuf把消息結果message也是通過 key-value對來表示。只是其中的key是採取一定的演算法計算出來的即通過每個message中每個欄位(field index)和欄位的數據類型進行運算得來的key = (index<<3)|type;
type類型的對應關係如下:
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會根據數據類型的不同會有兩種表現形式:
對於各種int,bool,enum類型,value就是Varint
對於string,bytes,message等等類型,value就是length+原始內容編碼
Varints是一種緊湊表示數字的方法。它用一個或者多個位元組表示一個數字,值越小的數字位元組數越少。相對於傳統的用4位元組表示int32類型數字,Varints對於小於128的數值都可以用一個位元組表示,大於128的數值會用更多的位元組來表示,對於很大的數據則需要用5個位元組來表示。
Varints演算法描述: 每一個位元組的最高位都是有特殊含義的,如果是1,則表示後續的位元組也是該數字的一部分;如果是0,則結束
b、demo生成的的二進位文件反序列化。
第1個位元組 (0A)
欄位索引(index): 0A = 0001010 0A>>3 = 001 = 1
數據類型(type): 0A = 0001010&111 = 2 (String);
第2個位元組 (0C)
字元串長度(length): 0E = 12;
字元串: 0A 05 69 64 6F 6C 33 10 01 18 BD 0F
第3個位元組 (0A)
因為字元串是來自phoneInfo屬於嵌套類型
欄位索引(index): 0A = 0001010 0A>>3 = 001 = 1
數據類型(type): 0A = 0001010&111 = 2 (String);
第4-9個位元組(69 64 6F 6C 33)
字元串長度(length): 05 = 5
字元串: 69 64 6F 6C 33 = idol3
第10個位元組 (10)
欄位索引(index): 10 = 00010000 10A>>3 = 0010 = 2
數據類型(type): 10 = 00010000&111 = 0 (Varints);
第11個位元組 (01)
Varints: 01 = 00001位元組的最高位為0 整數結束
Value: 1;
第12個位元組(18)
欄位索引(index): 18 = 00011000 18>> 00011 = 3
數據類型(type): 18 = 00011000&111 = 0 (Varints);
第13個位元組(D0)
最高位為1,整數計算到下一個位元組
第14個位元組(0F)
最高位為0,整數計算結束
Value:為11111010000 =2000
C、反序列化結果
phoneinfo為:
phoneName = “idol3”
top = 1
price = 2000;
同樣的方法watchInfo為:
watchName = “tcl name”
top = 1
price=2000
3、時間效率
通過protobuf序列化/反序列化的過程可以得出:protobuf是通過演算法生成二進位流,序列化與反序列化不需要解析相應的節點屬性和多餘的描述信息,所以序列化和反序列化時間效率較高。
4、空間效率
xml、json是用欄位名稱來確定類實例中欄位之間的獨立性,所以序列化後的數據多了很多描述信息,增加了序列化後的位元組序列的容量。
Protobuf的序列化/反序列化過程可以得出:
protobuf是由欄位索引(fieldIndex)與數據類型(type)計算(fieldIndex<<3|type)得出的key維護欄位之間的映射且只占一個位元組,所以相比json與xml文件,protobuf的序列化位元組沒有過多的key與描述符信息,所以占用空間要小很多。
七、Protobuf的源碼分析
1、protobuf在java使用的序列化流程
java程式調用parserFrom(byte[] data)開始位元組序列的反序列,Java程式通過調用編譯生類GenerateMessage中的wirteTo()方法開始將序列化後的位元組寫入輸出流中
GenerateMessage 繼承AbstractMessage類,序列化最終在AbstractMesssage中完成,序列化的實現過程:
a、遍歷對象中Message結構()
調用AbstractMessage類中的writeTo()方法
b、 序列化Message中每一個欄位
調用CodeOutputStream類中的writeMessageSetExtension()方法
c、 對於Varints Tag 的序列化流程:
調用CodeOutputStream類中的writeUInt32()方法
調用CodeOutputStream類中的WriteRawVarint32()方法
d、 對於非Varints Tag的序列化
調用CodeOutputStream類中的WriteTag()方法
具體的序列化實現都在CodedOutputStream中完成
2、java使用protobuf 的反序列化流程分析
java程式通過調用parserFrom(byte[] data)開始反序列化
具體在com.google.protobuf. AbstractParser類中實現
最後在com.google.protobuf.CodedInputStream類中完成反序列化
3、動態編譯
以windows下用protoc.exe工具實現proto文件編譯為例,protoc.exe是用C++實現。在控制台執行命令:
編譯的流程:
檢查proto的語法規則
將proto的文件中的message結構轉換為GenerateMessage類的子類,並實現Builder介面。
編譯流程
Main.cc中的main()方法
Command_line_interface.cc中的Run()方法
Import類中Import()
在Descriptor中完成message消息的收集和轉化。