Java中的Google协议缓冲区
協(xié)議緩沖區(qū)是一種用于結(jié)構(gòu)化數(shù)據(jù)的開源編碼機(jī)制。 它是由Google開發(fā)的,旨在實現(xiàn)語言/平臺中立且可擴(kuò)展。 在本文中,我的目的是介紹Java平臺上下文中協(xié)議緩沖區(qū)的基本用法。
Protobuff比XML更快,更簡單,并且比JSON更緊湊。 當(dāng)前,支持C ++,Java和Python。 但是,還有其他平臺(不是Google所支持的)作為開放源代碼項目–我嘗試了PHP實現(xiàn),但由于它尚未完全開發(fā),因此我停止使用它。 盡管如此,支持仍在繼續(xù)。 隨著Google宣布支持Google App Engine中的PHP,我相信他們會將其提升到一個新的水平。
基本上,您定義使用.proto規(guī)范文件來一次構(gòu)造數(shù)據(jù)的方式。 這類似于描述軟件組件的IDL文件或規(guī)范語言。 協(xié)議緩沖區(qū)編譯器(protoc)使用此文件,該協(xié)議緩沖區(qū)編譯器將生成支持方法,以便您可以在各種流中讀寫對象。
消息格式非常簡單。 每種消息類型都有一個或多個唯一編號的字段(稍后我們將介紹原因)。 嵌套消息類型具有其自己的唯一編號字段集。 值類型可以是數(shù)字,布爾值,字符串,字節(jié),集合和枚舉(受Java枚舉啟發(fā))。 另外,您可以嵌套其他消息類型,從而使您可以按照與JSON允許的方式幾乎相同的方式分層結(jié)構(gòu)化數(shù)據(jù)。
字段可以指定為可選 , 必需或重復(fù) 。 在Python中實現(xiàn)協(xié)議緩沖區(qū)時,不要讓字段的類型(例如enum,int32,float,string等)使您感到困惑。 在該領(lǐng)域的類型只是提示,protoc如何序列化的字段值,并產(chǎn)生你的郵件的郵件編碼格式(以后會更多)。 編碼格式看起來是對象的扁平化和壓縮表示形式。 無論您是在Python,Java還是C ++中使用協(xié)議緩沖區(qū),都將以完全相同的方式編寫此規(guī)范。
Protobuff是可擴(kuò)展的,您可以在以后的時間更新對象的結(jié)構(gòu),而不會破壞使用舊格式的程序。 如果要通過網(wǎng)絡(luò)發(fā)送數(shù)據(jù),則可以使用Protocol Buffer API對數(shù)據(jù)進(jìn)行編碼,然后序列化結(jié)果字符串。
可擴(kuò)展性這一概念非常重要,因為Java以及與此相關(guān)的許多其他序列化機(jī)制可能會存在互操作性和向后兼容性的問題。 使用這種方法,您不必?fù)?dān)心在代碼中維護(hù)表示對象結(jié)構(gòu)的serialVersionId字段。 維護(hù)該字段至關(guān)重要,因為Java的序列化機(jī)制將在反序列化對象時將其用作快速校驗和。 結(jié)果,一旦將對象序列化到某個文件系統(tǒng)或blob存儲中,以后就有可能對對象結(jié)構(gòu)進(jìn)行大刀闊斧的改變。 協(xié)議緩沖區(qū)受此影響較小。 只要您僅向?qū)ο筇砑涌蛇x字段,就可以反序列化舊類型,此時您可能會升級它們。
此外,您可以使用java_package關(guān)鍵字為.proto文件定義包名稱。 這樣可以很好地避免生成的代碼發(fā)生名稱沖突。 另一種選擇是像在下面的示例中一樣專門命名生成的類文件。 我在生成的類之前加上“ Proto”前綴,以表明這是一個生成的類。
這是一個簡單的消息規(guī)范,描述了帶有嵌入式地址消息User.proto的用戶:
option java_outer_classname="ProtoUser";message User {required int32 id = 1; // DB record IDrequired string name = 2;required string firstname = 3;required string lastname = 4;required string ssn= 5; // Embedded Address message specmessage Address {required int32 id = 1;required string country = 2 [default = "US"];; optional string state = 3;optional string city = 4;optional string street = 5;optional string zip = 6;enum Type {HOME = 0;WORK = 1; }optional Type addrType = 7 [default = HOME]; }repeated Address addr = 16; } 讓我們談?wù)劽總€屬性右側(cè)看到的標(biāo)簽號,因為它們非常重要。 這些標(biāo)記在此規(guī)范的對象上以二進(jìn)制表示形式標(biāo)識消息的字段順序。 標(biāo)記值1 – 15將被存儲為1個字節(jié),而標(biāo)記值16 – 2047的字段則需要2個字節(jié)進(jìn)行編碼-不能確定為什么這樣做。 Google建議您將標(biāo)簽1到15用于非常頻繁出現(xiàn)的數(shù)據(jù),并在此范圍內(nèi)保留一些標(biāo)簽值以用于將來的更新。
注意:不能使用數(shù)字19000到19999。保留用于原型實現(xiàn)。 另外,您可以定義必填,重復(fù)和可選的字段。從Google文檔中:
- required :格式正確的消息必須恰好具有此字段之一,即,嘗試使用未初始化的必填字段來構(gòu)建消息會引發(fā)RuntimeException。
- optional :格式正確的消息可以包含零個或一個此字段(但不能超過一個)。
- repeated :在格式正確的消息中,此字段可以重復(fù)任意次(包括零次)。 重復(fù)值的順序?qū)⒈A簟?
該文檔警告開發(fā)人員在使用required時要謹(jǐn)慎,因為如果您決定棄用一個字段,則這種類型的字段會引起問題。 這是所有序列化機(jī)制都會遇到的經(jīng)典向后兼容性問題。 Google工程師甚至建議對所有內(nèi)容使用可選。
此外,我指定了一個嵌套消息規(guī)范地址。 我可以輕松地將此定義放置在同一原型文件中的User對象之外。 因此,對于相關(guān)的消息定義,將它們?nèi)糠旁谕粋€.proto文件中是有意義的。 即使“地址”消息類型不是一個很好的例子,但是如果消息類型在其“父”對象之外不存在,我將使用嵌套類型。 例如,如果您要序列化LinkedList的Node 。 那么在這種情況下,節(jié)點將是嵌入式消息定義。 這取決于您和您的設(shè)計。
可選的消息屬性被忽略時采用默認(rèn)值。 特別是,使用特定于類型的默認(rèn)值代替:對于字符串,默認(rèn)值為空字符串;對于字符串,默認(rèn)值為空字符串。 對于布爾值,默認(rèn)值為false; 對于數(shù)字類型,默認(rèn)值為零; 對于枚舉,默認(rèn)值是枚舉類型定義中列出的第一個值(這很酷,但不太明顯)。
枚舉非常好。 它們跨平臺的工作方式與Java中的enum幾乎相同。 枚舉字段的值可以只是一個值。 您可以在消息定義內(nèi)部或外部聲明枚舉,就好像它是自己的獨(dú)立實體一樣。 如果在消息類型內(nèi)指定,則可以通過[Message-name]。[enum-name]公開另一種消息類型。
協(xié)議
針對.proto文件運(yùn)行協(xié)議緩沖區(qū)編譯器時,編譯器將生成用于所選語言的代碼。 它將您的消息類型轉(zhuǎn)換為增強(qiáng)類,其中包括為屬性提供getter和setter等。 編譯器還生成方便的方法,以在輸出流和字符串之間來回串行化消息。
對于枚舉類型,生成的代碼將具有一個對應(yīng)的Java或C ++枚舉,或者一個特殊的Python EnumDescriptor類,該類用于在運(yùn)行時生成的類中創(chuàng)建帶有整數(shù)值的符號常量集。
對于Java,編譯器將為每種消息類型生成具有流利的Design Builder類的.java文件,以簡化對象的創(chuàng)建和初始化。 編譯器生成的消息類是不可變的。 一旦建立,便無法更改。
您可以在參考資料部分中閱讀有關(guān)其他平臺(Python,C ++)的信息,并在此處詳細(xì)介紹字段編碼:
https://developers.google.com/protocol-buffers/docs/reference/overview。
對于我們的示例,我們將使用–java_out命令行標(biāo)志調(diào)用protoc。 該標(biāo)志向編譯器指示生成的Java類的輸出目錄-每個原型文件一個Java類。
API
生成的API為以下便捷方法提供支持:
- isInitialized()
- toString()
- mergeFrom(...)
- 明確()
對于解析和序列化:
- byte [] toByteArray()
- parseFrom()
- writeTo(OutputStream)在示例代碼中用于編碼
- parseFrom(InputStream)在示例代碼中用于解碼
樣例代碼
讓我們建立一個簡單的項目。 我喜歡遵循Maven的默認(rèn)原型:
protobuff-example / src / main / java / [應(yīng)用程序代碼] protobuff-example / src / main / java / gen [生成的原型類] protobuff-example / src / main / proto [原型文件定義]
為了生成協(xié)議緩沖區(qū)類,我將執(zhí)行以下命令:
# protoc --proto_path=/home/user/workspace/eclipse/trunk/protobuff/--java_out=/home/user/workspace/eclipse/trunk/protobuff/src/main/java /home/user/workspace/eclipse/trunk/protobuff/src/main/proto/User.proto我將展示一些生成的代碼,并簡要介紹它們。 生成的類很大,但是很容易理解。 它將提供構(gòu)建器來創(chuàng)建用戶和地址的實例。
public final class ProtoUser {public interface UserOrBuilderextends com.google.protobuf.MessageOrBuilder...public interface AddressOrBuilderextends com.google.protobuf.MessageOrBuilder {....}生成的類包含用于真正流暢地創(chuàng)建對象的Builder接口。 這些構(gòu)建器接口在原型文件中指定的每個屬性都有g(shù)etter和setter,例如:
public String getCountry() {java.lang.Object ref = country_;if (ref instanceof String) {return (String) ref;} else {com.google.protobuf.ByteString bs =(com.google.protobuf.ByteString) ref;String s = bs.toStringUtf8();if (com.google.protobuf.Internal.isValidUtf8(bs)) {country_ = s;}return s;}}由于這是一種自定義編碼機(jī)制,因此邏輯上所有字段都具有自定義字節(jié)包裝器。 我們的簡單String字段在存儲時使用ByteString進(jìn)行壓縮,然后將其反序列化為UTF-8字符串。
// required int32 id = 1;public static final int ID_FIELD_NUMBER = 1;private int id_;public boolean hasId() {return ((bitField0_ & 0x00000001) == 0x00000001);}在這次電話會議中,我們看到了開頭提到的標(biāo)簽號的重要性。 這些標(biāo)簽號似乎代表某種位位置,這些位位置定義了數(shù)據(jù)在字節(jié)串中的位置。 接下來,我們看一下前面提到的write和read方法的代碼片段。
將實例寫入輸出流:
public void writeTo(com.google.protobuf.CodedOutputStream output)throws java.io.IOException {getSerializedSize();if (((bitField0_ & 0x00000001) == 0x00000001)) {output.writeInt32(1, id_);}if (((bitField0_ & 0x00000002) == 0x00000002)) {output.writeBytes(2, getCountryBytes()); .... }從輸入流中讀取:
public static ProtoUser.User parseFrom(java.io.InputStream input)throws java.io.IOException {return newBuilder().mergeFrom(input).buildParsed(); }此類約為2000行代碼。 還有其他詳細(xì)信息,例如如何映射Enum類型以及如何存儲重復(fù)的類型。 希望我提供的代碼片段可以使您對該類的結(jié)構(gòu)有一個較高的了解。
讓我們看一些使用生成的類的應(yīng)用程序級代碼。 要保留數(shù)據(jù),我們可以簡單地執(zhí)行以下操作:
// Create instance of AddressAddress addr = ProtoUser.User.Address.newBuilder() .setAddrType(Address.Type.HOME) .setCity("Weston").setCountry("USA").setId(1).setState("FL").setStreet("123 Lakeshore").setZip("90210").build();// Serialize instance of UserUser user = ProtoUser.User.newBuilder() .setId(1).setFirstname("Luis").setLastname("Atencio").setName("luisat").setSsn("555-555-5555") .addAddr(addr).build();// Write fileFileOutputStream output = new FileOutputStream("target/user.ser"); user.writeTo(output); output.close();一旦堅持下來,我們可以這樣讀:
User user = User.parseFrom(new FileInputStream("target/user.ser");System.out.println(user);要運(yùn)行示例代碼,請使用:
java -cp。:../ lib / protobuf-java-2.4.1.jar app.Serialize ../target/user.ser
Protobuff與XML
Google聲稱協(xié)議緩沖區(qū)比XML快20到100倍(以納秒為單位),而刪除空白則小3到10倍。 但是,直到所有平臺(不僅是上述3種平臺)都得到支持和采用,XML仍將繼續(xù)成為非常流行的序列化機(jī)制。 此外,并非每個人都具有Google用戶對性能的要求和期望。 XML的替代方法是JSON。
Protobuff與JSON
我進(jìn)行了一些比較測試,以評估在JSON上使用協(xié)議緩沖區(qū)的性能。 結(jié)果令人震驚,一個簡單的測試顯示,就存儲而言,原型增益器的效率提高了50%以上。 我創(chuàng)建了一個簡單的POJO版本的User-Address類,并使用GSON庫對一個實例進(jìn)行了編碼,該實例的狀態(tài)與上述示例相同(我將省略實現(xiàn)細(xì)節(jié),請檢查下面引用的gson項目)。 編碼相同的用戶數(shù)據(jù),我得到:
-rw-rw-r-- 1 luisat luisat 206 May 30 09:47 json-user.ser -rw-rw-r-- 1 luisat luisat 85 May 30 09:42 user.ser這很了不起。 我也在另一個博客中找到了它(請參閱下面的資源):
絕對值得一讀。
結(jié)論和進(jìn)一步說明
協(xié)議緩沖區(qū)可能是跨平臺數(shù)據(jù)編碼的良好解決方案。 使用Java,Python,C ++和其他許多語言編寫的客戶端,存儲/發(fā)送壓縮數(shù)據(jù)非常簡單。
一個棘手的觀點是:“永遠(yuǎn)記住需要的信息。” 如果您發(fā)瘋了,并且需要.proto文件的每個字段,那么刪除或編輯這些字段將非常困難。
同樣有一點激勵作用,即在Google的數(shù)據(jù)存儲中使用probbuff :在Google的代碼樹中,跨12,183個.proto文件定義了48,162種不同的消息類型。
協(xié)議緩沖區(qū)促進(jìn)了良好的面向?qū)ο笤O(shè)計,因為.proto文件基本上是愚蠢的數(shù)據(jù)持有者(如C ++中的結(jié)構(gòu))。 根據(jù)Google文檔,如果您想向生成的類添加更豐富的行為,或者您無法控制.proto文件的設(shè)計,則最好的方法是將生成的協(xié)議緩沖區(qū)類包裝在應(yīng)用程序中,具體類別。
最后,請記住,永遠(yuǎn)不要通過從生成的類繼承行為來向它們添加行為。 這將破壞內(nèi)部機(jī)制,無論如何都不是一個好的面向?qū)ο蟮膶嵺`。
這里介紹的許多信息來自個人經(jīng)驗,其他資源,最重要的是來自Google開發(fā)人員代碼。 請在參考資料部分中查閱文檔。
資源資源
參考:我們的JCG合作伙伴 Luis Atencio的Java協(xié)議緩沖區(qū) ,在Reflective Thought博客上。
翻譯自: https://www.javacodegeeks.com/2012/06/google-protocol-buffers-in-java.html
總結(jié)
以上是生活随笔為你收集整理的Java中的Google协议缓冲区的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安卓通知中心在哪里(安卓通知中心)
- 下一篇: 物业保安备案流程(物业保安备案)