轉載自http://www.cnblogs.com/stephen-liu74/archive/2013/01/02/2841485.htmlhtml
該系列Blog的內容主體主要源自於Protocol Buffer的官方文檔,而代碼示例則抽取於當前正在開發的一個公司內部項目的Demo。這樣作的目的主要在於不只能夠保持Google文檔的良好風格和系統性,同時再結合一些比較實用和通用的用例,這樣就更加便於公司內部的培訓,以及和廣大網友的技術交流。須要說明的是,Blog的內容並不是line by line的翻譯,其中包含一些經驗性總結,與此同時,對於一些不是很是經常使用的功能並未予以說明,有興趣的開發者能夠直接查閱Google的官方文檔。java
1、爲何使用Protocol Buffer?
在回答這個問題以前,咱們仍是先給出一個在實際開發中常常會遇到的系統場景。好比:咱們的客戶端程序是使用Java開發的,可能運行自不一樣的平臺,如:Linux、Windows或者是Android,而咱們的服務器程序一般是基於Linux平臺並使用C++開發完成的。在這兩種程序之間進行數據通信時存在多種方式用於設計消息格式,如:
1. 直接傳遞C/C++語言中一字節對齊的結構體數據,只要結構體的聲明爲定長格式,那麼該方式對於C/C++程序而言就很是方便了,僅需將接收到的數據按照結構體類型強行轉換便可。事實上對於變長結構體也不會很是麻煩。在發送數據時,也只需定義一個結構體變量並設置各個成員變量的值以後,再以char*的方式將該二進制數據發送到遠端。反之,該方式對於Java開發者而言就會很是繁瑣,首先須要將接收到的數據存於ByteBuffer之中,再根據約定的字節序逐個讀取每一個字段,並將讀取後的值再賦值給另一個值對象中的域變量,以便於程序中其餘代碼邏輯的編寫。對於該類型程序而言,聯調的基準是必須客戶端和服務器雙方均完成了消息報文構建程序的編寫後才能展開,而該設計方式將會直接致使Java程序開發的進度過慢。即使是Debug階段,也會常常遇到Java程序中出現各類域字段拼接的小錯誤。
2. 使用SOAP協議(WebService)做爲消息報文的格式載體,由該方式生成的報文是基於文本格式的,同時還存在大量的XML描述信息,所以將會大大增長網絡IO的負擔。又因爲XML解析的複雜性,這也會大幅下降報文解析的性能。總之,使用該設計方式將會使系統的總體運行性能明顯降低。
對於以上兩種方式所產生的問題,Protocol Buffer都可以很好的解決,不只如此,Protocol Buffer還有一個很是重要的優勢就是能夠保證同一消息報文新舊版本之間的兼容性。至於具體的方式咱們將會在後續的博客中給出。python
2、定義第一個Protocol Buffer消息。
建立擴展名爲.proto的文件,如:MyMessage.proto,並將如下內容存入該文件中。git
message LogonReqMessage { required int64 acctID = 1; required string passwd = 2; }
這裏將給出以上消息定義的關鍵性說明。
1. message是消息定義的關鍵字,等同於C++中的struct/class,或是Java中的class。
2. LogonReqMessage爲消息的名字,等同於結構體名或類名。
3. required前綴表示該字段爲必要字段,既在序列化和反序列化以前該字段必須已經被賦值。與此同時,在Protocol Buffer中還存在另外兩個相似的關鍵字,optional和repeated,帶有這兩種限定符的消息字段則沒有required字段這樣的限制。相比於optional,repeated主要用於表示數組字段。具體的使用方式在後面的用例中均會一一列出。
4. int64和string分別表示長整型和字符串型的消息字段,在Protocol Buffer中存在一張類型對照表,既Protocol Buffer中的數據類型與其餘編程語言(C++/Java)中所用類型的對照。該對照表中還將給出在不一樣的數據場景下,哪一種類型更爲高效。該對照表將在後面給出。
5. acctID和passwd分別表示消息字段名,等同於Java中的域變量名,或是C++中的成員變量名。
6. 標籤數字1和2則表示不一樣的字段在序列化後的二進制數據中的佈局位置。在該例中,passwd字段編碼後的數據必定位於acctID以後。須要注意的是該值在同一message中不能重複。另外,對於Protocol Buffer而言,標籤值爲1到15的字段在編碼時能夠獲得優化,既標籤值和類型信息僅佔有一個byte,標籤範圍是16到2047的將佔有兩個bytes,而Protocol Buffer能夠支持的字段數量則爲2的29次方減一。有鑑於此,咱們在設計消息結構時,能夠儘量考慮讓repeated類型的字段標籤位於1到15之間,這樣即可以有效的節省編碼後的字節數量。另外 19000 到 19999 也不能用。他們是protobuf 的編譯預留標籤。github
3、定義第二個(含有枚舉字段)Protocol Buffer消息。編程
//在定義Protocol Buffer的消息時,可使用和C++/Java代碼一樣的方式添加註釋。 enum UserStatus { OFFLINE = 0; //表示處於離線狀態的用戶 ONLINE = 1; //表示處於在線狀態的用戶 } message UserInfo { required int64 acctID = 1; required string name = 2; required UserStatus status = 3; }
這裏將給出以上消息定義的關鍵性說明(僅包括上一小節中沒有描述的)。
1. enum是枚舉類型定義的關鍵字,等同於C++/Java中的enum。
2. UserStatus爲枚舉的名字。
3. 和C++/Java中的枚舉不一樣的是,枚舉值之間的分隔符是分號,而不是逗號。
4. OFFLINE/ONLINE爲枚舉值。
5. 0和1表示枚舉值所對應的實際整型值,和C/C++同樣,能夠爲枚舉值指定任意整型值,而無需老是從0開始定義。如:數組
enum OperationCode { LOGON_REQ_CODE = 101; LOGOUT_REQ_CODE = 102; RETRIEVE_BUDDIES_REQ_CODE = 103; LOGON_RESP_CODE = 1001; LOGOUT_RESP_CODE = 1002; RETRIEVE_BUDDIES_RESP_CODE = 1003; }
4、定義第三個(含有嵌套消息字段)Protocol Buffer消息。服務器
咱們能夠在同一個.proto文件中定義多個message,這樣即可以很容易的實現嵌套消息的定義。如:網絡
enum UserStatus { OFFLINE = 0; ONLINE = 1; } message UserInfo { required int64 acctID = 1; required string name = 2; required UserStatus status = 3; } message LogonRespMessage { required LoginResult logonResult = 1; required UserInfo userInfo = 2; }
這裏將給出以上消息定義的關鍵性說明(僅包括上兩小節中沒有描述的)。
1. LogonRespMessage消息的定義中包含另一個消息類型做爲其字段,如UserInfo userInfo。
2. 上例中的UserInfo和LogonRespMessage被定義在同一個.proto文件中,那麼咱們是否能夠包含在其餘.proto文件中定義的message呢?Protocol Buffer提供了另一個關鍵字import,這樣咱們即可以將不少通用的message定義在同一個.proto文件中,而其餘消息定義文件能夠經過import的方式將該文件中定義的消息包含進來,如:
import "myproject/CommonMessages.proto"eclipse
5、限定符(required/optional/repeated)的基本規則。
1. 在每一個消息中必須至少留有一個required類型的字段。
2. 每一個消息中能夠包含0個或多個optional類型的字段。
3. repeated表示的字段能夠包含0個或多個數據。須要說明的是,這一點有別於C++/Java中的數組,由於後二者中的數組必須包含至少一個元素。
4. 若是打算在原有消息協議中添加新的字段,同時還要保證老版本的程序可以正常讀取或寫入,那麼對於新添加的字段必須是optional或repeated。道理很是簡單,老版本程序沒法讀取或寫入新增的required限定符的字段。
6、類型對照表。
.proto Type | Notes | C++ Type | Java Type |
double | double | double | |
float | float | float | |
int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int |
int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long |
uint32 | Uses variable-length encoding. | uint32 | int |
uint64 | Uses variable-length encoding. | uint64 | long |
sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int |
sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long |
fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 228. | uint32 | int |
fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 256. | uint64 | long |
sfixed32 | Always four bytes. | int32 | int |
sfixed64 | Always eight bytes. | int64 | long |
bool | bool | boolean | |
string | A string must always contain UTF-8 encoded or 7-bit ASCII text. | string | String |
bytes | May contain any arbitrary sequence of bytes. | string | ByteString |
7、Protocol Buffer消息升級原則。
在實際的開發中會存在這樣一種應用場景,既消息格式由於某些需求的變化而不得不進行必要的升級,可是有些使用原有消息格式的應用程序暫時又不能被馬上升級,這便要求咱們在升級消息格式時要遵照必定的規則,從而能夠保證基於新老消息格式的新老程序同時運行。規則以下:
1. 不要修改已經存在字段的標籤號。
2. 任何新添加的字段必須是optional和repeated限定符,不然沒法保證新老程序在互相傳遞消息時的消息兼容性。
3. 在原有的消息中,不能移除已經存在的required字段,optional和repeated類型的字段能夠被移除,可是他們以前使用的標籤號必須被保留,不能被新的字段重用。
4. int3二、uint3二、int6四、uint64和bool等類型之間是兼容的,sint32和sint64是兼容的,string和bytes是兼容的,fixed32和sfixed32,以及fixed64和sfixed64之間是兼容的,這意味着若是想修改原有字段的類型時,爲了保證兼容性,只能將其修改成與其原有類型兼容的類型,不然就將打破新老消息格式的兼容性。
5. optional和repeated限定符也是相互兼容的。
8、Packages。
咱們能夠在.proto文件中定義包名,如:
package ourproject.lyphone;
該包名在生成對應的C++文件時,將被替換爲名字空間名稱,既namespace ourproject { namespace lyphone。而在生成的Java代碼文件中將成爲包名。
9、Options。
Protocol Buffer容許咱們在.proto文件中定義一些經常使用的選項,這樣能夠指示Protocol Buffer編譯器幫助咱們生成更爲匹配的目標語言代碼。Protocol Buffer內置的選項被分爲如下三個級別:
1. 文件級別,這樣的選項將影響當前文件中定義的全部消息和枚舉。
2. 消息級別,這樣的選項僅影響某個消息及其包含的全部字段。
3. 字段級別,這樣的選項僅僅響應與其相關的字段。
下面將給出一些經常使用的Protocol Buffer選項。
1. option java_package = "com.companyname.projectname";
java_package是文件級別的選項,經過指定該選項可讓生成Java代碼的包名爲該選項值,如上例中的Java代碼包名爲com.companyname.projectname。與此同時,生成的Java文件也將會自動存放到指定輸出目錄下的com/companyname/projectname子目錄中。若是沒有指定該選項,Java的包名則爲package關鍵字指定的名稱。該選項對於生成C++代碼毫無影響。
2. option java_outer_classname = "LYPhoneMessage";
java_outer_classname是文件級別的選項,主要功能是顯示的指定生成Java代碼的外部類名稱。若是沒有指定該選項,Java代碼的外部類名稱爲當前文件的文件名部分,同時還要將文件名轉換爲駝峯格式,如:my_project.proto,那麼該文件的默認外部類名稱將爲MyProject。該選項對於生成C++代碼毫無影響。
注:主要是由於Java中要求同一個.java文件中只能包含一個Java外部類或外部接口,而C++則不存在此限制。所以在.proto文件中定義的消息均爲指定外部類的內部類,這樣才能將這些消息生成到同一個Java文件中。在實際的使用中,爲了不老是輸入該外部類限定符,能夠將該外部類靜態引入到當前Java文件中,如:import static com.company.project.LYPhoneMessage.*。
3. option optimize_for = LITE_RUNTIME;
optimize_for是文件級別的選項,Protocol Buffer定義三種優化級別SPEED/CODE_SIZE/LITE_RUNTIME。缺省狀況下是SPEED。
SPEED: 表示生成的代碼運行效率高,可是由今生成的代碼編譯後會佔用更多的空間。
CODE_SIZE: 和SPEED偏偏相反,代碼運行效率較低,可是由今生成的代碼編譯後會佔用更少的空間,一般用於資源有限的平臺,如Mobile。
LITE_RUNTIME: 生成的代碼執行效率高,同時生成代碼編譯後的所佔用的空間也是很是少。這是以犧牲Protocol Buffer提供的反射功能爲代價的。所以咱們在C++中連接Protocol Buffer庫時僅需連接libprotobuf-lite,而非libprotobuf。在Java中僅需包含protobuf-java-2.4.1-lite.jar,而非protobuf-java-2.4.1.jar。
注:對於LITE_MESSAGE選項而言,其生成的代碼均將繼承自MessageLite,而非Message。
4. [pack = true]: 由於歷史緣由,對於數值型的repeated字段,如int3二、int64等,在編碼時並無獲得很好的優化,然而在新近版本的Protocol Buffer中,可經過添加[pack=true]的字段選項,以通知Protocol Buffer在爲該類型的消息對象編碼時更加高效。如:
repeated int32 samples = 4 [packed=true]。
注:該選項僅適用於2.3.0以上的Protocol Buffer。
5. [default = default_value]: optional類型的字段,若是在序列化時沒有被設置,或者是老版本的消息中根本不存在該字段,那麼在反序列化該類型的消息是,optional的字段將被賦予類型相關的缺省值,如bool被設置爲false,int32被設置爲0。Protocol Buffer也支持自定義的缺省值,如:
optional int32 result_per_page = 3 [default = 10]。
10、命令行編譯工具。
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto
這裏將給出上述命令的參數解釋。
1. protoc爲Protocol Buffer提供的命令行編譯工具。
2. --proto_path等同於-I選項,主要用於指定待編譯的.proto消息定義文件所在的目錄,該選項能夠被同時指定多個。
3. --cpp_out選項表示生成C++代碼,--java_out表示生成Java代碼,--python_out則表示生成Python代碼,其後的目錄爲生成後的代碼所存放的目錄。
4. path/to/file.proto表示待編譯的消息定義文件。
注:對於C++而言,經過Protocol Buffer編譯工具,能夠將每一個.proto文件生成出一對.h和.cc的C++代碼文件。生成後的文件能夠直接加載到應用程序所在的工程項目中。如:MyMessage.proto生成的文件爲MyMessage.pb.h和MyMessage.pb.cc。
代碼實例
參考官網例子,想要完成此項工做主要分三步:
1.在.proto 中定義消息格式;
2.使用protocol buffer編譯程序;
3.使用Java protocol buffer API讀寫消息;
1、定義消息格式
package tutorial; option java_package = "com.example.tutorial"; option java_outer_classname = "AddressBookProtos"; message Person { required string name = 1; required int32 id = 2; optional string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { required string number = 1; optional PhoneType type = 2 [default = HOME]; } repeated PhoneNumber phone = 4; } message AddressBook { repeated Person person = 1; }
2、編譯協議文件
1.下載編譯器,去github下載,我下載的是window版 https://github.com/google/protobuf/releases/tag/v3.0.0
2.解壓文件,bin文件夾裏面有一個protoc.exe執行文件。
3.protoc-3.0.0-win32文件夾下新建兩個文件夾java-做爲文件生成路徑;proto-做爲協議路徑
4.dos中執行:protoc -I=../proto --java_out=../java ../proto/addressbook.proto
5.查看java文件夾,文件已經生成
3、使用Java protocol buffer API讀寫消息
1.新建maven java工程,pom信息以下:
<span style="white-space:pre"> </span><dependency> <span style="white-space:pre"> </span><groupId>com.google.protobuf</groupId> <span style="white-space:pre"> </span><artifactId>protobuf-java</artifactId> <span style="white-space:pre"> </span><version>3.0.0</version> <span style="white-space:pre"> </span></dependency>
2.拷貝生成的java文件到工程中
3.新建一個測試類(寫)
package com.example.tutorial; import com.example.tutorial.AddressBookProtos.AddressBook; import com.example.tutorial.AddressBookProtos.Person; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.InputStreamReader; import java.io.IOException; import java.io.PrintStream; class AddPerson { // This function fills in a Person message based on user input. static Person PromptForAddress(BufferedReader stdin, PrintStream stdout) throws IOException { Person.Builder person = Person.newBuilder(); stdout.print("Enter person ID: "); person.setId(Integer.valueOf(stdin.readLine())); stdout.print("Enter name: "); person.setName(stdin.readLine()); stdout.print("Enter email address (blank for none): "); String email = stdin.readLine(); if (email.length() > 0) { person.setEmail(email); } while (true) { stdout.print("Enter a phone number (or leave blank to finish): "); String number = stdin.readLine(); if (number.length() == 0) { break; } Person.PhoneNumber.Builder phoneNumber = Person.PhoneNumber.newBuilder().setNumber(number); stdout.print("Is this a mobile, home, or work phone? "); String type = stdin.readLine(); if (type.equals("mobile")) { phoneNumber.setType(Person.PhoneType.MOBILE); } else if (type.equals("home")) { phoneNumber.setType(Person.PhoneType.HOME); } else if (type.equals("work")) { phoneNumber.setType(Person.PhoneType.WORK); } else { stdout.println("Unknown phone type. Using default."); } person.addPhone(phoneNumber); } return person.build(); } // Main function: Reads the entire address book from a file, // adds one person based on user input, then writes it back out to the same // file. public static void main(String[] args) throws Exception { if (args.length != 1) { System.err.println("Usage: AddPerson ADDRESS_BOOK_FILE"); System.exit(-1); } AddressBook.Builder addressBook = AddressBook.newBuilder(); // Read the existing address book. try { addressBook.mergeFrom(new FileInputStream(args[0])); } catch (FileNotFoundException e) { System.out.println(args[0] + ": File not found. Creating a new file."); } // Add an address. addressBook.addPerson( PromptForAddress(new BufferedReader(new InputStreamReader(System.in)), System.out)); // Write the new address book back to disk. FileOutputStream output = new FileOutputStream(args[0]); addressBook.build().writeTo(output); output.close(); } }
測試類作的是新建一個文件來保存序列化的AddressBook信息,固然新建AddressBook須要輸入保存的文件地址及person內容。
eclipse中 Run->Run Configurations->Arguments 填寫C:\Users\Administrator\Desktop\AddressBook
運行...
輸入一個person laowang、一個person laozhang,可見桌面上生成了一個AddressBook文件
C:\Users\Administrator\Desktop\AddressBook: File not found. Creating a new file.
Enter person ID: 1
Enter name: laowang
Enter email address (blank for none): laowang@163.com
Enter a phone number (or leave blank to finish):
...
4.新建一個測試類(讀)
package com.example.tutorial; import com.example.tutorial.AddressBookProtos.AddressBook; import com.example.tutorial.AddressBookProtos.Person; import java.io.FileInputStream; import java.io.IOException; import java.io.PrintStream; class ListPeople { // Iterates though all people in the AddressBook and prints info about them. static void Print(AddressBook addressBook) { for (Person person: addressBook.getPersonList()) { System.out.println("Person ID: " + person.getId()); System.out.println(" Name: " + person.getName()); if (person.hasEmail()) { System.out.println(" E-mail address: " + person.getEmail()); } for (Person.PhoneNumber phoneNumber : person.getPhoneList()) { switch (phoneNumber.getType()) { case MOBILE: System.out.print(" Mobile phone #: "); break; case HOME: System.out.print(" Home phone #: "); break; case WORK: System.out.print(" Work phone #: "); break; } System.out.println(phoneNumber.getNumber()); } } } // Main function: Reads the entire address book from a file and prints all // the information inside. public static void main(String[] args) throws Exception { if (args.length != 1) { System.err.println("Usage: ListPeople ADDRESS_BOOK_FILE"); System.exit(-1); } // Read the existing address book. AddressBook addressBook = AddressBook.parseFrom(new FileInputStream(args[0])); Print(addressBook); } }
運行時輸入參數(文件保存路徑),能夠看到打印的信息:Person ID: 1 Name: laowang E-mail address: laowang@163.com Mobile phone #: 15811111111Person ID: 2 Name: laozhang E-mail address: laozhang@163.com Mobile phone #: 15822222222好啦,入門級的測試完成了,如今回頭看看給咱們生成的AddressBookProtos.java中都提供了哪些函數。標準方法:一.每一個Message和Builder中包含如下方法1)isInitialized(): 檢查是否全部的必須屬性已經被設置.2)toString(): 你懂得.3)mergeFrom(Message other): (只在builder中) 覆蓋已有的單屬性,合併重複屬性.4)clear(): (只在builder中) 清空全部的屬性值到空狀態.二.序列化和反序列化(這個看方法名就知道了)1)byte[] toByteArray();: serializes the message and returns a byte array containing its raw bytes.2)static Person parseFrom(byte[] data);: parses a message from the given byte array.3)void writeTo(OutputStream output);: serializes the message and writes it to an OutputStream.4)static Person parseFrom(InputStream input);: reads and parses a message from an InputStream.