協議緩沖區是一種用于結構化數據的開源編碼機制。 它是由Google開發的,旨在實現語言/平臺中立且可擴展。 在本文中,我的目的是介紹Java平臺上下文中協議緩沖區的基本用法。
Protobuff比XML更快,更簡單,并且比JSON更緊湊。 當前,支持C ++,Java和Python。 但是,還有其他平臺(不是Google所支持的)作為開放源代碼項目–我嘗試了PHP實現,但由于它尚未完全開發,因此我停止使用它。 盡管如此,支持仍在繼續。 隨著Google宣布支持Google App Engine中的PHP,我相信他們會將其提升到一個新的水平。
基本上,您定義使用.proto規范文件來一次構造數據的方式。 這類似于描述軟件組件的IDL文件或規范語言。 協議緩沖區編譯器(protoc)使用此文件,該協議緩沖區編譯器將生成支持方法,以便您可以在各種流中讀寫對象。
消息格式非常簡單。 每種消息類型都有一個或多個唯一編號的字段(稍后我們將介紹原因)。 嵌套消息類型具有其自己的唯一編號字段集。 值類型可以是數字,布爾值,字符串,字節,集合和枚舉(受Java枚舉啟發)。 另外,您可以嵌套其他消息類型,從而使您可以按照與JSON允許的方式幾乎相同的方式分層結構化數據。
字段可以指定為可選 , 必需或重復 。 在Python中實現協議緩沖區時,不要讓字段的類型(例如enum,int32,float,string等)使您感到困惑。 在該領域的類型只是提示,protoc如何序列化的字段值,并產生你的郵件的郵件編碼格式(以后會更多)。 編碼格式看起來是對象的扁平化和壓縮表示形式。 無論您是在Python,Java還是C ++中使用協議緩沖區,都將以完全相同的方式編寫此規范。
Protobuff是可擴展的,您可以在以后的時間更新對象的結構,而不會破壞使用舊格式的程序。 如果要通過網絡發送數據,則可以使用Protocol Buffer API對數據進行編碼,然后序列化結果字符串。
可擴展性這一概念非常重要,因為Java以及與此相關的許多其他序列化機制可能會存在互操作性和向后兼容性的問題。 使用這種方法,您不必擔心在代碼中維護表示對象結構的serialVersionId字段。 維護該字段至關重要,因為Java的序列化機制將在反序列化對象時將其用作快速校驗和。 結果,一旦將對象序列化到某個文件系統或blob存儲中,以后就有可能對對象結構進行大刀闊斧的改變。 協議緩沖區受此影響較小。 只要您僅向對象添加可選字段,就可以反序列化舊類型,此時您可能會升級它們。
此外,您可以使用java_package關鍵字為.proto文件定義包名稱。 這樣可以很好地避免生成的代碼發生名稱沖突。 另一種選擇是像在下面的示例中一樣專門命名生成的類文件。 我在生成的類之前加上“ Proto”前綴,以表明這是一個生成的類。
這是一個簡單的消息規范,描述了帶有嵌入式地址消息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;
}
讓我們談談每個屬性右側看到的標簽號,因為它們非常重要。 這些標記在此規范的對象上以二進制表示形式標識消息的字段順序。 標記值1 – 15將被存儲為1個字節,而標記值16 – 2047的字段則需要2個字節進行編碼-不能確定為什么這樣做。 Google建議您將標簽1到15用于非常頻繁出現的數據,并在此范圍內保留一些標簽值以用于將來的更新。
注意:不能使用數字19000到19999。保留用于原型實現。 另外,您可以定義必填,重復和可選的字段。從Google文檔中:
-
required
:格式正確的消息必須恰好具有此字段之一,即,嘗試使用未初始化的必填字段來構建消息會引發RuntimeException。 -
optional
:格式正確的消息可以包含零個或一個此字段(但不能超過一個)。 -
repeated
:在格式正確的消息中,此字段可以重復任意次(包括零次)。 重復值的順序將保留。
該文檔警告開發人員在使用required時要謹慎,因為如果您決定棄用一個字段,則這種類型的字段會引起問題。 這是所有序列化機制都會遇到的經典向后兼容性問題。 Google工程師甚至建議對所有內容使用可選。
此外,我指定了一個嵌套消息規范地址。 我可以輕松地將此定義放置在同一原型文件中的User對象之外。 因此,對于相關的消息定義,將它們全部放在同一個.proto文件中是有意義的。 即使“地址”消息類型不是一個很好的例子,但是如果消息類型在其“父”對象之外不存在,我將使用嵌套類型。 例如,如果您要序列化LinkedList的Node 。 那么在這種情況下,節點將是嵌入式消息定義。 這取決于您和您的設計。
可選的消息屬性被忽略時采用默認值。 特別是,使用特定于類型的默認值代替:對于字符串,默認值為空字符串;對于字符串,默認值為空字符串。 對于布爾值,默認值為false; 對于數字類型,默認值為零; 對于枚舉,默認值是枚舉類型定義中列出的第一個值(這很酷,但不太明顯)。
枚舉非常好。 它們跨平臺的工作方式與Java中的enum幾乎相同。 枚舉字段的值可以只是一個值。 您可以在消息定義內部或外部聲明枚舉,就好像它是自己的獨立實體一樣。 如果在消息類型內指定,則可以通過[Message-name]。[enum-name]公開另一種消息類型。
協議
針對.proto文件運行協議緩沖區編譯器時,編譯器將生成用于所選語言的代碼。 它將您的消息類型轉換為增強類,其中包括為屬性提供getter和setter等。 編譯器還生成方便的方法,以在輸出流和字符串之間來回串行化消息。
對于枚舉類型,生成的代碼將具有一個對應的Java或C ++枚舉,或者一個特殊的Python EnumDescriptor類,該類用于在運行時生成的類中創建帶有整數值的符號常量集。
對于Java,編譯器將為每種消息類型生成具有流利的Design Builder類的.java文件,以簡化對象的創建和初始化。 編譯器生成的消息類是不可變的。 一旦建立,便無法更改。
您可以在參考資料部分中閱讀有關其他平臺(Python,C ++)的信息,并在此處詳細介紹字段編碼:
https://developers.google.com/protocol-buffers/docs/reference/overview。
對于我們的示例,我們將使用–java_out命令行標志調用protoc。 該標志向編譯器指示生成的Java類的輸出目錄-每個原型文件一個Java類。
API
生成的API為以下便捷方法提供支持:
- isInitialized()
- toString()
- mergeFrom(...)
- 明確()
對于解析和序列化:
- byte [] toByteArray()
- parseFrom()
- writeTo(OutputStream)在示例代碼中用于編碼
- parseFrom(InputStream)在示例代碼中用于解碼
樣例代碼
讓我們建立一個簡單的項目。 我喜歡遵循Maven的默認原型:
protobuff-example / src / main / java / [應用程序代碼] protobuff-example / src / main / java / gen [生成的原型類] protobuff-example / src / main / proto [原型文件定義]
為了生成協議緩沖區類,我將執行以下命令:
# 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
我將展示一些生成的代碼,并簡要介紹它們。 生成的類很大,但是很容易理解。 它將提供構建器來創建用戶和地址的實例。
public final class ProtoUser {public interface UserOrBuilderextends com.google.protobuf.MessageOrBuilder...public interface AddressOrBuilderextends com.google.protobuf.MessageOrBuilder {....}
生成的類包含用于真正流暢地創建對象的Builder接口。 這些構建器接口在原型文件中指定的每個屬性都有getter和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;}}
由于這是一種自定義編碼機制,因此邏輯上所有字段都具有自定義字節包裝器。 我們的簡單String字段在存儲時使用ByteString進行壓縮,然后將其反序列化為UTF-8字符串。
// required int32 id = 1;public static final int ID_FIELD_NUMBER = 1;private int id_;public boolean hasId() {return ((bitField0_ & 0x00000001) == 0x00000001);}
在這次電話會議中,我們看到了開頭提到的標簽號的重要性。 這些標簽號似乎代表某種位位置,這些位位置定義了數據在字節串中的位置。 接下來,我們看一下前面提到的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行代碼。 還有其他詳細信息,例如如何映射Enum類型以及如何存儲重復的類型。 希望我提供的代碼片段可以使您對該類的結構有一個較高的了解。
讓我們看一些使用生成的類的應用程序級代碼。 要保留數據,我們可以簡單地執行以下操作:
// 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);
要運行示例代碼,請使用:
java -cp。:../ lib / protobuf-java-2.4.1.jar app.Serialize ../target/user.ser
Protobuff與XML
Google聲稱協議緩沖區比XML快20到100倍(以納秒為單位),而刪除空白則小3到10倍。 但是,直到所有平臺(不僅是上述3種平臺)都得到支持和采用,XML仍將繼續成為非常流行的序列化機制。 此外,并非每個人都具有Google用戶對性能的要求和期望。 XML的替代方法是JSON。
Protobuff與JSON
我進行了一些比較測試,以評估在JSON上使用協議緩沖區的性能。 結果令人震驚,一個簡單的測試顯示,就存儲而言,原型增益器的效率提高了50%以上。 我創建了一個簡單的POJO版本的User-Address類,并使用GSON庫對一個實例進行了編碼,該實例的狀態與上述示例相同(我將省略實現細節,請檢查下面引用的gson項目)。 編碼相同的用戶數據,我得到:
-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
這很了不起。 我也在另一個博客中找到了它(請參閱下面的資源):

絕對值得一讀。
結論和進一步說明
協議緩沖區可能是跨平臺數據編碼的良好解決方案。 使用Java,Python,C ++和其他許多語言編寫的客戶端,存儲/發送壓縮數據非常簡單。
一個棘手的觀點是:“永遠記住需要的信息。” 如果您發瘋了,并且需要.proto文件的每個字段,那么刪除或編輯這些字段將非常困難。
同樣有一點激勵作用,即在Google的數據存儲中使用probbuff :在Google的代碼樹中,跨12,183個.proto文件定義了48,162種不同的消息類型。
協議緩沖區促進了良好的面向對象設計,因為.proto文件基本上是愚蠢的數據持有者(如C ++中的結構)。 根據Google文檔,如果您想向生成的類添加更豐富的行為,或者您無法控制.proto文件的設計,則最好的方法是將生成的協議緩沖區類包裝在應用程序中,具體類別。
最后,請記住,永遠不要通過從生成的類繼承行為來向它們添加行為。 這將破壞內部機制,無論如何都不是一個好的面向對象的實踐。
這里介紹的許多信息來自個人經驗,其他資源,最重要的是來自Google開發人員代碼。 請在參考資料部分中查閱文檔。
資源資源
- https://developers.google.com/protocol-buffers/docs/overview
- https://developers.google.com/protocol-buffers/docs/proto
- https://developers.google.com/protocol-buffers/docs/reference/java-generated
- https://developers.google.com/protocol-buffers/docs/reference/overview
- http://code.google.com/p/google-gson/
- http://afrozahmad.hubpages.com/hub/protocolbuffers
參考:我們的JCG合作伙伴 Luis Atencio的Java協議緩沖區 ,在Reflective Thought博客上。
翻譯自: https://www.javacodegeeks.com/2012/06/google-protocol-buffers-in-java.html