Protocol Buffers的应用与分析
Protocol Buffers的應用與分析 明塵
1? Protocol Buffers的介紹
Protocol Buffers是一種用于序列化結構化數據的機制,它具有靈活、高效、自動化的特點。類似于XML,但是比XML更小巧、快捷、簡單。在Google?幾乎所有它內部的RPC協議和文件格式都是采用PB。
PB具有以下特點:
在這里,我做了個小實驗,將一個29230KB的自定義格式的文本數據轉換成PB和XML:
| ? | PB | XML |
| 轉換后的大小 | 21011KB | 43202KB |
| 解析時間(100次循環) | 18610ms | 169251ms |
| 完成解析所寫代碼行數 | 1行 | 50行 |
| 與官方說法的差距,主要可能是因為應用場景不同,我的測試數據中字段比較長 | ||
表1:PB與XML的實驗比較
可見,PB作為一種輕量級的數據協議,在時間、空間上都有一定的優勢。
2? Protocol Buffers的簡單應用
2.1? 創建流程
2.1.1? 定義一個.proto文件
新建一個文件,命名為addressbook.proto,內容如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | package tutorial;//命名空間 option java_package = "com.example.tutorial";//生成文件的包名 option java_outer_classname = "AddressBookProtos";//類名 message Person { //要描述的結構化數據 ????required string name = 1;//required表示這個字段不能為空 ????required int32 id = 2;//等號后面的內容為數字別名 ????optional string email = 3;//optional表示可以為空 ????PhoneNumber {//內部message ????????required string number = 1; ????????optional int32 type = 2; ????} ????repeated PhoneNumber phone = 4 } message AddressBook { ????repeated Person person = 1;//是個集合 } |
對以上內容的一點解釋:
- PB所支持的元類型數據請參考:PB元類型數據
- 修飾符required:這個修飾符應該謹慎使用,濫用會導致后續的修改容易出現兼容性問題;
- 修飾符optional:對于常出現的屬性,為節省空間應該取1-16的別名;
- PB是以key-value的形式來將結構化數據序列化的。它采用了將等號后的數字別名以及屬性的類型用varints編碼成一個數字,來作為key。
2.1.2? 使用PB編譯器
輸入:protoc????? -I=$SRC_DIR –java_out=$DST_DIR $SRC_DIR/addressbook.proto
其中??? -I指定.proto文件所在目錄
–java_out指定生成java文件所在的目錄
2.1.3? 使用PB的API來寫入和讀取messages
經過以上步驟,會在指定的$DST_DIR目錄下生成一個AddressBookProtos.java的類。在maven中引入protobuf-java這個依賴后,利用這個類,便能序列化/反序列化數據了。
生成的代碼結構如下:
| 1 2 3 4 5 6 7 | class AddressBookProtos{ ????class Person{ ????????class PhoneNumber{class Builder{} } ????????class Builder{} ????} ????class AddressBook{class Builder{} } } |
可以看到Person、PhoneNumber、AddressBook這些內部類則對應了所定義的那些message。
2.2? 序列化數據及分析
通過閱讀代碼可以看到,以上三個類的成員變量都是private類型的,并且,只提供了getter方法,而沒有提供setter方法去為數據變量賦值。
PB利用了內部類可以訪問到外部類中私有成員變量的特性。對外部類的任何賦值操作都需要通過Builder內部類來進行。Builder中有一個指向外部類的引用(名為result),當賦值完成,調用Builder的build()方法時,會把這個對象返回,同時使result指向null。
PB通過這樣一種方式保證了數據安全性,一旦數據構建完畢,將無法再對其進行修改。
拿PhoneNumber這個類來說,對成員變量number、type賦值,需要以如下方式來進行:
| 1 2 3 4 5 6 7 | PhoneNumber.Builder builder = PhoneNumber.newBuilder(); //調用setter賦值,setter返回了this,所以可以鏈式表述 builder.setNumber("111").setType(1); //賦值完成后,調用Builder的build方法,將返回PhoneNumber對象 PhoneNumber phoneNumber = builder.build(); |
構建完成后,可以調用writeTo方法,將數據寫入數據流中。
2.3? 反序列化及分析
一行代碼便能完成反序列化:
| 1 | AddressBook ?list = AddressBook .parseFrom(inputStream或buffer); |
背后PB做了很多事情:
首先調用CodedInputStream的readTag,也就是從中取得key值(int類型),然后通過swtich塊來往對象中賦值(PB采用了Base 128 Varints的方式來編碼這個數字,后面會介紹這種方式的)。
3? message的編碼特點
PB之所以解析速度快、所占體積小,很大程度上是由它序列化的編碼特點來決定的。
3.1 Base 128 Varints
PB采用了Base 128 Varints來變長編碼整數:
例子:
300 ??varints? 編碼為:1010 1100 0000 0010
解釋如下:
300的2進制編碼為:0001 0010 1100
按照剛才的規則,高低位顛倒,截取最后的7為放在第一個byte,則第一byte為1010 1100(其中最高位1表示,后續還有byte);接著剩下的內容放到第二個byte,為0000 0010(其中最高位0表示,后續無byte,這個數到這里截止了)。
于是,合在一起為 1010 1100 0000 0010;
3.2 Key-Value
如前所述,PB的message是一系列的key-value對,在二進制數據中,使用varints數字(包含了別名以及屬性類型信息)來作為key,進而通過由PB編譯器生成的代碼來構造以及解析數據。
PB將 key編碼成下面的結構:
X YYYY ZZZ
其中:最高位X表示是否還有后續的byte來編碼數字別名;YYYY用于編碼別名,定義了多余16個屬性,則需要用到額外的byte,所以出現頻率高的字段應當取1-16的別名);ZZZ表示這個字段的類型,PB支持的屬性的對應規則如下表:
| 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, floa |
表2:PB 屬性對應規則
例子:
required int32 a=1;? 在應用中給a賦值150?? ,序列化后08 96 01
- 08代表的是key 0 0001 000, 最高位為0,表示這個key為一個byte,中間四位表示a的數字別名,最后三位表示a的屬性類型;
- 96 01代表的是value,二進制為:1001 0110 0000 0001
→ 001 0110??? 000 0001(去掉最高位)
→ 22????????????? +? 1*2^7 = 150
3.3 Zig-Zag
采用varints的方式編碼有符號的整數,效率比較差,因為負數的最高位是1,這樣就導致了情況類似于編碼一個很大的數。
為了解決這個問題,Protocol Buffers定義了sint32/sint64屬性,他們采用了“之字形”(ZigZag)編碼的方式,將負數編碼成正數,交替進行。看了下表就很好理解了:
| Signed Original | Encoded As |
| 0 | 0 |
| -1 | 1 |
| 1 | 2 |
| -2 | 3 |
| 2147483647 | 4294967294 |
| 2147483648 | 4294967295 |
表3:Zig-Zag編碼規則
利用這個方式,可以有效地節省存儲空間,也能提高解析效率。
了解了以上內容,對于其他數據類型的編碼,也是很好理解的,大家可以參考官方文檔,這里不做詳述。
4 其他
官方文檔中,有提到PB提供了RPC的接口,但是沒有提供具體實現。當在的.proto文件中,加入如下定義:
| 1 2 3 | service XXX { ????rpc MMM(request) returns(response); } |
PB便會為你生成一個代表這個服務的XXX虛類,通過實現這個類中的abstract MMM方法,以及提供RpcChannel的實現,你便可以利用Protocol Buffers實現你的RPC了。
第三方的RPC實現大家可以參考ThirdPartyRPC
在這里,我利用了第三方實現protobuf-socket-rpc,寫了一個小例子,有興趣的可以看看。如下:Protocol buffer的rpc例子
5 小結
PB具有跨平臺、解析速度快、序列化數據體積小、擴展性高、使用簡單的特點。但是我們也可以看到,相比于XML,PB的數據,并不是自然可讀的;同時它生成的代碼不是純pojo,對于代碼有一定的侵入性。在你的項目中,如果對于以上缺點要求并不高,可以嘗試著使用PB。
總結
以上是生活随笔為你收集整理的Protocol Buffers的应用与分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Google Protocol Buff
- 下一篇: java 中的override ove