這篇博客既是幫助一些初學者深入理解protocolBuffer,也是爲了方便自己記憶和進一步學習。本文主要介紹了三個方面,包括:
· 在一個.proto文件裏面如何定義消息格式
· 如何使用protocol buffer的編譯器
· 如何使用java protocol buffer的API來讀寫消息
首先,讓我們來了解一下爲什麼要使用protocolBuffer?
假設我們現在要做一個叫做“address book”的應用程序來發送和接收人們的通訊錄,這個通訊錄包含了人們的姓名、身份證號碼、電子郵箱、和一系列的聯繫方式(包括手機電話等等)。
那麼你如何序列化並且檢索這些結構化的數據呢?常規的有以下幾種做法:
1.使用Java的序列化接口,這是我們在使用Java編程時候的常規做法,但是Java的序列化方式卻有着許多缺點(這裏暫不深究)。並且它在與用其他面嚮對象語言編寫的程序(例如:C++,Python)進行數據共享的時候也存在着許許多多的問題。
2.你可以用一種特殊的方法把一些數據編碼成一個字符串-例如把四個整數編碼成這種形式“12:3:23:67”。這是一種非常簡單並且靈活的方法,不過它需要按照這種特殊格式來進行解碼,那麼解碼工作就需要一定的時間開銷。但是這對於一些比較簡單的消息來說無非是最好的方法。
3.把這些數據序列化然後寫入XML。自從許多語言都爲XML建立語言庫之後,這種方法無疑是非常吸引人的。當你需要與其他的工程或者應用程序進行數據共享的時候,這無疑是一個很好的選擇。然而,衆所周知的是,XML的文字空間過於密集,編碼和解碼無疑成了應用程序的一個巨大工程。並且,要操作一系列相關的XML文檔集,要比操作一些類集領域要複雜的多。
所以,Google公司就研發了靈活度非常高,並且能夠高效的自動解決這些問題的protocolBuffer。使用protocol buffer的時候,你只需要寫一個.proto文件來定義你需要結構化並且保存的數據,protocol buffer的編譯器就會自動的爲你解析protocol buffer數據(protocol buffer使用的是二進制的數據格式)並且完成編碼工作。它生成的類爲protocol buffer提供了getters和setters,並且特別的將定義reading和writing protocol buffer數據的方法作爲一個單元。需要注意的是,protocol buffer格式支持超時的數據,並且仍然可以讀取使用以前的格式編寫的代碼。
說了這麼多,那就來真實的感受一下吧,就拿之前說到的“address book”的應用程序來舉例,首先:
創建一個.proto文件並在裏面定義消息格式,把你需要序列化的數據在裏面聲明類型和名稱,並將這個文件命名爲addressbook.proto。具體內容如下:
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;
}
正如你所看到的,它的語法類似C++或者Java,讓我們通過該文件的每一個部分來分析一下吧。
首先,這個文件以一個package的定義開頭,以防在不同工程中的命名衝突,在Java裏面,package就是用來當做Java package(除非你有明確的定義一個java package)。 不過,在這裏,即使你已經提供了一個java_package,你仍然需要定義一個package以防Protocol Buffer使用在其他沒有Java語言的環境中。
在package的定義之後,你可以看到兩個option(選項):java_package以及java_outer_classname。java_package是用來定義java類應該在哪個包裏面生成,如果你沒有寫這一項的話,那麼他默認的會以你的package的定義來生成。Java_outer_classname這一項是用來定義在這個文件中的哪一個類需要包含所有的這些類的信息,如果你不定義這一項的話,那麼它會以你的文件名來作爲刻板,比如說如果你寫的是“my_proto.proto”的話,那麼它就會默認的使用“MyProto”來作爲類名。
接下來就是你消息的類型定義了。一個message就是一系列類型領域的集合,許多基礎的數據類型在這裏面都是可用的,包括bool,int32,float,double,以及string等。你同樣可以添加更多的結構化的數據在裏面,比如上面的Person message就包含了PhoneNumber message,而AddressBook message包含了Person message。你同樣在一中message類型裏面定義另外一種類型,例如上面所舉到的,Person裏面所定義的enum(枚舉)類型,以區分PhoneNumber的不同類型。
在每一個元素之後的“=1”,“=2”是用來區分它們的獨特的“標籤”。標籤數字1-15編碼所需的字節數比更高的數字所需的字節數要少一個,所以爲了使程序達到最佳的狀態,你可以使用這些標籤進行反覆標記(在同一個域中不能重複)。每一個元素在重複的領域都需要重新編碼這些“標籤”,所以重複的領域應該考慮到更多可能的方案來達到最佳狀態。
每一個域的前面都必須使用下面這些修飾符來修飾:
·required(必需的): 這說明這個域的值不能爲空,否則這條message將會被當做“不知情的”。如果你嘗試的創建一條“不知情的”message,那麼系統將會拋出一個RuntimeException(運行時異常)。而分析一條“不知情的”message則會拋出IOException。除此之外,確切的來說一個被required修飾的域其行爲則更接近optional修飾的域。
·optional(可選擇的): 被這個修飾符修飾的域可以爲空。如果一個被optional修飾的域沒有設值的話,那麼系統將會使用默認值。對於一些基本類型來說,你可以定義你自己的默認值(就像我前面在定義PhoneNumber的PhoneType時一樣)。 否則,對於數值類型來說,系統的默認值是0,string的默認值是empty string,bool的默認值是false。對於植入的message來說(比如AddressBook裏面的Person),默認值則經常是該消息默認的實例或者標準。調用存取器去獲取那些被optional(或者required)修飾的但還沒有被初始化的域將會返回它的默認值。
·repeated(反覆的): 某一個域可能會被使用多次,而那些反覆使用的值將會被保留在protocol buffer裏面。 你可以把用repeated修飾的域想象成動態數組。
Required是“永久”的。
當你使用required來修飾域的時候你必須非常的小心。如果某些時候你想要停止發送一個用required修飾的域並將它修改爲optional修飾時,之前的readers會把你的message考慮爲不完整的並且無意識的丟棄它。事實上,人們已經開始注意到了使用required的所帶來的危害,所以我們應該更多的使用optional和repeated。
使用Protocol buffer的編譯器
現在,你已經有一個.proto文件了,接下來就需要生成class來發送和讀取AddressBook消息(包含Person以及PhoneNumber),爲了完成這件工作,你需要調用protocol buffer的編譯器的proto指令來編譯你的.proto文件。語法如下:
proto -I=$SRC --java_out=$DIR File
$SRC表示資源文件夾,$DIR表示目標文件夾,File是你要編譯的.proto文件,當然,你也可以定位到這個資源目錄中然後只調用proto --java_out=$DIR File即可。(需要注意的是生成的class文件會存在於你所填的目標文件夾下的java_package所指向的目錄中,如果你想把目標文件夾指向當前目錄,你可以使用“.”來代替,例如對於該例,我們先定位到這個目錄下,然後運行proto --java_out=. addressbook.proto):
使用protocol buffer的API
首先,讓我們看看編譯器給我們生成了那些代碼。首先你可以發現它的類名與我們定義的java_outer_classname的名字相同,同樣裏面還包含了你在addressbook.proto裏面定義的各種message的類,每一個類都有它自身的Builder用來實例化這個類對象。所有的messages和builders都自動生成了存取器,但是messages只有getter而builders既有getters又有setters。以下就是Person類的一些存取器:
// required string name = 1;
public boolean hasName();
public String getName();
// required int32 id = 2;
public boolean hasId();
public int getId();
// optional string email = 3;
public boolean hasEmail();
public String getEmail();
// repeated .tutorial.Person.PhoneNumber phone = 4;
public List<PhoneNumber> getPhoneList();
public int getPhoneCount();
public PhoneNumber getPhone(int index);
同樣的, Person.Builder 也有這樣的存取器:
// required string name = 1;
public boolean hasName();
public java.lang.String getName();
public Builder setName(String value);
public Builder clearName();
// required int32 id = 2;
public boolean hasId();
public int getId();
public Builder setId(int value);
public Builder clearId();
// optional string email = 3;
public boolean hasEmail();
public String getEmail();
public Builder setEmail(String value);
public Builder clearEmail();
// repeated .tutorial.Person.PhoneNumber phone = 4;
public List<PhoneNumber> getPhoneList();
public int getPhoneCount();
public PhoneNumber getPhone(int index);
public Builder setPhone(int index, PhoneNumber value);
public Builder addPhone(PhoneNumber value);
public Builder addAllPhone(Iterable<PhoneNumber> value);
public Builder clearPhone();
正如你所看到的一樣,這些都是一些簡單的JavaBean-style的getters和setters,但是用repeated修飾的域有一些特殊的方法,Count方法(用來統計這個消息list(列表)的長度)。通過add 方法可以在這個list中追加一個元素,而addAll 方法可以把一個Container(容器)裏面的所有元素都添加在list當中。
注意到這些方法都是使用的駝峯式命名法,儘管在.proto文件裏面我們都是寫的小寫,這也恰恰展示了protocol buffer的強大之處。
另外,還有一個需要注意的就是enum(枚舉)類型所生成的類,它自動生成了如下代碼:
public static enum PhoneType {
MOBILE(0, 0),
HOME(1, 1),
WORK(2, 2), ; ...}
Builders vs. Messages
Message類裏面由protocol buffer編譯器自動生成的代碼都是不可變的,一旦一個message對象被實例化之後,它就不能再被修改了,就像Java中的String一樣。而如果想要實例化一個message類,你必須首先實例化一個builder類,然後設置好所有你想要設置的屬性,然後再調用builder類的build()方法。你或許已經注意到了builder的每一個用來改變message的屬性的方法都返回了另外一個builder。不要懷疑,這個 builder就是爲了讓你更加方便的定義其他屬性而存在的。下面就展示了一段用來創建一個新的Person類的代碼:
Person john = Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("[email protected]")
.addPhone( Person.PhoneNumber.newBuilder().setNumber("555-4321")
.setType(Person.PhoneType.HOME))
.build();
標準的Message方法
每一個message以及builder類都包含了一些其他的方法用來檢測和操作所有的message,這些方法包括:
·isInitialized(): 檢測所有用required修飾的域是否都設置了初始值。
·toString(): 返回一個可讀的message,特別是用來測試的時候 。
·mergeFrom(Message other): (builder獨有的) 把另一個message合併到這個message當中,重寫單獨域並且連接反覆的域。
·clear(): (builder獨有的)清空所有的域並且返回空的狀態
這些方法都實現了Message和Message.Builder 接口並且接口被所有的Java messages以及builders共享。
解析和序列化
最後,所有的protocol buffer類都有writing和reading你所選擇的protocol buffer (二進制)格式數據的方法。他們包括:
·byte[] toByteArray();: 序列化這個 message 並且返回一個字節數組。
·static Person parseFrom(byte[] data);: 從給定的字節數組中解析一條message。
·void writeTo(OutputStream output);: 序列化這個message並且將它寫入 OutputStream.
·static Person parseFrom(InputStream input);: 讀取並且解析一條InputStream中的message
這裏只是其中的幾個解析和序列化的方法而已,如果想要知道其中所有的方法可以將該類生成doc文檔然後查看。
Protocol Buffers和O-O Design
Protocol buffer的類基本上是無聲的數據持有者(就像 C++裏面的結構體一樣);他們最開始在一個對象模型中表現的並不友好,如果你想要爲生成的類添加一個更加友好的方法的話,最好的方式是將protocol buffer生成的類包裝在一個特定的應用程序裏面。對於不太懂.proto文件設計的人來說包裝protocol buffer也是一個不錯的主意。你也可以使用包裝類生成接口以便更好地適應特定的程序環境:隱藏一些數據和方法,並且顯示一些便捷的功能,等等。特別需要注意的是,你最好不要寫一些行爲去繼承這些生成的類。那會打破它內部的機制況且它對於你來說也不是一次很好的面向對象的練習機會。
Writing A Message
OK,說了這麼多,那就來使用一下protocol buffer生成的類吧。首先,你肯定希望你的這個“addressbook”的應用程序可以write一個你定義的message。爲了完成這項工作,你需要創建一個新的類來調用protocol buffer類裏面的方法並將message寫入OutputStream.
下面就是一個將用戶在控制檯輸入的”AddressBook” 的相關信息寫入文件的一個類,當然,你首先得創建一個文件(當然你也可以在文件不存在的情況下使用File類的createNewFile()方法來創建一個新的文件),爲了個性化你的程序,不妨以.book作爲你的後綴名,具體代碼如下:
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import com.example.tutorial.AddressBookProtos.AddressBook;
import com.example.tutorial.AddressBookProtos.Person;
class AddPerson {
/**
* 將用戶輸入的Person message寫入輸出流中
* @param stdin 輸入流
* @param stdout 打印輸出流
* @return Person類
* @throws IOException
*/
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) {
//按下Enter鍵結束輸入
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();
// 檢驗是否存在這個文件
try {
addressBook.mergeFrom(new FileInputStream("src/Book/TestPerson.book"));
} catch (FileNotFoundException e) {
System.out.println("src/Book/TestPerson.book" + ": File not found.Creating a new file.");
}
//將這條Person message添加到AddressBook中
addressBook.addPerson(PromptForAddress(new BufferedReader(new InputStreamReader(System.in)),System.out));
//將新建的AddressBook寫入文件當中
FileOutputStream output = new FileOutputStream("src/Book/TestPerson.book");
addressBook.build().writeTo(output);
output.close();
}
}
Reading A Message
當然,這個程序肯定不止一個寫入消息的類,還要能把存在文件中的數據讀出來。如下:
import com.example.tutorial.AddressBookProtos.AddressBook;
import com.example.tutorial.AddressBookProtos.Person;
import java.io.FileInputStream;
class ListPeople {
/**
* 迭代遍歷並且打印文件中所包含的信息
* @param addressBook AddressBook對象
*/
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());
}
}
}
public static void main(String[] args) throws Exception {
// if (args.length != 1) {
// System.err.println("Usage: ListPeople ADDRESS_BOOK_FILE");
// System.exit(-1);
// }
// 讀取已經存在.book文件
AddressBook addressBook = AddressBook.parseFrom(new FileInputStream("src/Book/TestPerson.book"));
Print(addressBook);
}
}
擴展一個Protocol Buffer
當你發佈了一段由你的protocol buffer編寫的代碼之後,你或許迫不及待的想要擴展它的功能。如果你想要使你的新buffers是反向兼容的或者你的舊buffers是正向兼容的話,那麼下面有幾條規則是你需要遵守的,在新的protocol buffer的版本中:· 你最好不要改變已經存在的域的標籤(Tag)
·你最好不要添加或者刪除任何用required修飾的域
·你可以刪除optional或者repeated修飾的域
·你可以添加新的用optional或者repeated修飾的域但你必須使用Tag數字(從未被這個protocol所使用過的tag,即使是被刪除了的也不行).
如果你遵循這些規則,舊的代碼也會非常“高興”的讀取新的消息。對於舊的代碼來說,那些被刪除了的用optional修飾的域會有他們的默認值並且被刪除了用repeated修飾的域會爲空,新的代碼讀取舊的消息也會很輕鬆。然而,請記住,新的optional域不會存在於舊的message當中,所以你應該明確的知道它們是否被設置爲has_, 或者在你的.proto 文件提供了一個合理的默認值[default = value]。如果默認值沒有明確是一個optional元素,而是按默認類型定義的話: 對於string來說, 默認值就是empty string,其他類型也類型,在本文的上面已經提到過了,這裏就不在累贅。特別聲明,如果你添加了一個新的用repeated修飾的域而沒有has_標誌的話,你的新代碼將無法識別它是否爲空,而你的舊代碼則完全無法設置它。
高級用法
Protocol buffers還有一些用法是一般的存取器和序列化所無法辦到的,如果你需要了解更多的信息可以上Google的官方文檔上面去查看。
Protocol的message類提供的一個關鍵特徵就是映射。 在一個message裏面你可以反覆聲明不同的域並且操作他們的值而不需要在任何類型的message之前寫代碼。使用映射來從其他的編碼中轉換一條message無疑是一個非常有效的方法,即使面對XML 或者JSON也一樣。關於映射的一個更加高級的用法恐怕就是找出兩條相同類型的message類之間的不同點了,或者是爲Protocol buffer生成一系列的“規則映像”,使用這些映像你可以匹配確切的消息內容。發揮你的想象,Protocol Buffers可以應用到更多的領域當中去。
(材料取自:Google官方文檔)
PS:由於可視化編輯器的問題,本來想用顏色着重標記一些重點的,不過對這個感興趣的人也會好好的看一看吧。