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