最近忽然對RPC序列化感興趣,可是發現Protobuf的資料並很少,因而在官網找到了Java使用Protocol Buffer的入門指南,用蹩腳的英文翻譯了下,以饗同道。原文地址html
示例:一個簡單的通信簿, .proto 文件見 addressbook.proto。java
syntax = "proto2";
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 phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
複製代碼
.proto 文件開始聲明包,以避免出現命名衝突。 在 JAVA 中,該包名能夠做爲java中的package,除非您又專門指定了 java_package,在addressbook.proto 中咱們就指定了package。git
即便您指定了一個java_package,也應該須要定義一個常規的package,是爲了在Protocol Buffers命名空間中產生衝突。github
在包定義聲明以後,還看到了兩個java規範中的可選項: java_package、java_outer_classname。 java_package 指定了生成的java類須要放在哪一個包下面,若是沒有指定這個值,則會使用package指定的值。 java_outer_classname 選項定義了類名,包含這個.proto文件裏面的全部類,若是沒有明確指定這個值的話,將會把文件名經過駝峯大小寫命名方式做爲類名, 例如 my_proto.proto 則默認會生成 MyProto 類名。正則表達式
接下來,就有了消息(message)定義。編程
一個消息(message)是包含一系列類型字段的聚合。數組
不少標準的簡單數據類型供字段可用,包含:maven
您還能夠添加其餘的結構類型爲您的消息字段類型所用。前面的示例中,Person 消息包含了PhoneNumber消息,而AddressBook消息包含Person消息。 您還能夠定義枚舉類型enum,若是你想你的字段可能的值爲一個事先預約好的列表中的值,就可使用enum類型。 這個電話簿示例中,phone number有三種類型: MOBILE、HOME、WORK。編程語言
=1、=2 表示每一個元素上的標識字段在二進制編碼中使用的惟一「標記」。 Tag編號 1-15 編號所需的字節比編號高的要少,因此您能夠對經常使用的或常常重複使用的標記使用這些編號,剩下的16到更高的標記在可選元素中比較少用。 重複字段中的每一個元素都須要從新編碼標記號tag,因此重複的字段是這種優化的最佳選擇。ide
每一個字段必須標註爲如下幾種修飾符:
required :該字段必須提供,不然認爲消息是"未初始化"的。嘗試去構建一個未初始化的消息會拋出一個RuntimeException。解析一個未初始化的消息則會拋出一個IOException。除此以外,required字段的行爲和optional字段徹底同樣。
optional :表示可選的字段,該字段能夠設值亦能夠不設值。若是一個可選字段沒有set值,則會使用其默認值進行初始化。對於簡單類型,您能夠明確指定本身默認的值,就像咱們在示例中處理的同樣(phoneNumber的type字段)。不然,系統默認值:數值類型默認爲0,字符類型默認爲空串,boo類型默認爲false。對於嵌入的消息,默認值老是"默認實例"或者消息的"原型",沒有設置任何字段。
repeated :該字段能夠重複任意次數使用。重複值的順序將會保留在protocol buffer中。能夠將重複字段看做動態數組。
Required Is Forever 您須要很是謹慎地將字段標記爲required修飾。若是在某些時候,您但願中止寫或發送一個required字段,在將該字段變動爲optional時可能會發生問題——舊的reader認爲消息沒有這個值則會拒絕或者丟棄這個消息。您應該爲考慮爲緩衝區編寫特定於應用程序地校驗例程。有一些Google工程師推測使用required弊大於利。他們更傾向於使用opyional和repeated。無論怎樣,這種觀點並不廣泛。
您還能夠在Protocol Buffer 語言指南中瞭解到完整地教程。 不要嘗試去尋找相似繼承的工具,protocol buffer不支持這樣作。
如今您有了一個 .proto 文件了,下一步要作的事,就是生成一個您將要讀、寫的AddressBook類。所以,您須要運行potocol buffer編譯器 protoc 處理 .proto:
Protocol 編譯器是C++編寫的。若是您使用C++,請根據C++安裝指導安裝protoc。 對於非C++用戶,最簡單的安裝protocol編譯器的方式是從release頁下載預構建的二進制:github.com/protocolbuf…
下載好的 protoc-$VERSION-$PLATFORM.zip
。包含了二進制protoc,還有一系列與protobuf發佈的標準的.proto文件。 若是您還想找舊版本,能夠在https://repo1.maven.org/maven2/com/google/protobuf/protoc/找到。
這些預構建的二進制文件只會在發行版本中提供。
Protobuf 支持幾種不一樣的編程語言。針對於每一種語言,你能夠參考源碼中的各類語言的說明指導。
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto
由於您想生成Java類,您看到 --java_out 選項,相似的選項也提供了其餘編程語言。
這將會在您指定的目標目錄生成com/example/tutorial/AddressBookProtos.java。
讓咱們來看看一些生成的代碼,並查看一些由編譯器爲您建立的類與方法。若是您看一下AddressBookProtos.java 類,能夠看到定義了一個叫作AddressBookProtos的類,內嵌了您在addressbooproto文件中爲每一個消息指定的類。每一個類都擁有自身的Builder類,您可使用它來建立一個對應的類實例。您能夠在下面的 Builders vs. Messages能夠看到更多細節。
messages 和 builders 擁有針對消息每一個字段的自動生成的訪問方法。message僅僅擁有getters方法、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 phones = 4;
public List<PhoneNumber> getPhonesList();
public int getPhonesCount();
public PhoneNumber getPhones(int index);
複製代碼
同時, Person.Builder 內部類則擁有getters、setters:
// 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 phones = 4;
public List<PhoneNumber> getPhonesList();
public int getPhonesCount();
public PhoneNumber getPhones(int index);
public Builder setPhones(int index, PhoneNumber value);
public Builder addPhones(PhoneNumber value);
public Builder addAllPhones(Iterable<PhoneNumber> value);
public Builder clearPhones();
複製代碼
正如您所看到的同樣,每一個字段都有簡單的 Java Bean 風格的getter、setter 方法。 每一個字段也有其has方法,若是設置了字段值,則會返回true。 最後,每一個字段還有clear方法,能夠將該字段迴歸到其空白狀態。
repeated 字段由一些額外的方法:
注意到全部這些訪問方法都使用了駝峯命名方式,即便 .proto 文件使用了小寫和下劃線。這個轉換是由protocol編譯器自動完成的,因此生成的類符合Java風格標準規範。在 .proto 中您應該老是將字段名用小寫和下劃線來命名。 能夠參考風格指南獲取更多良好的 .proto 命名風格。 更多關於編譯器爲字段生成的特定的詳細信息能夠參考 Java生成代碼參考指南
生成的代碼包含了一個枚舉 PhoneType ,嵌套在 Person 類中:
public static enum PhoneType {
MOBILE(0, 0),
HOME(1, 1),
WORK(2, 2),
;
...
}
複製代碼
內部類Person.PhoneNumber也生成了,正如您指望的同樣,它是做爲Person的一個內部類的。
protocol buffer編譯器生成的類都是不可變的。一旦一個message對象被建立後,就不能再被更改,相似於Java的String類。要構建一個message,您必須構建一個builder,並設置任何您想設置的字段的值,再調用builders's 的builder 方法。(使用過lombok的朋友,這很相似其@Builder註解的用法)
您可能還注意到每一個builder的方法返回的是另外一個builder。返回的對象其實和您調用方法的builder是同一個。這種處理方式很方便,您能夠將多個setter在一行代碼中串寫。
這裏有一個示例,建立一個Person的實例:
Person john =
Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("jdoe@example.com")
.addPhones(
Person.PhoneNumber.newBuilder()
.setNumber("555-4321")
.setType(Person.PhoneType.HOME)
.build())
.build();
複製代碼
每一個message和builder類也包含一系列其餘的方法,可讓您檢查與操做message:
最後,每一個 proto buffer 類都有一些用於讀和寫二進制的方法。
這些只是爲序列化和解析(反序列化)而提供的兩組操做方法。更多完整列表能夠參考 Message API幫助文檔。
Protocol Buffers 和 面向對象 Protocol buffer 類基本上是啞數據持有者(相似C中的struct);在對象模型中,它們不是一等公民。若是您想向生成類中添加更豐富的行爲,最好的方式就是在一個特定於應用程序中的類中包裝protocol buffer生成的類。若是您不能控制.proto文件的設計(例如,若是您正在重用來自另外一個項目的一個文件),那麼包裝協議緩衝區也是一個好主意。在這種狀況下,您可使用包裝器類來建立更適合您的應用程序的獨特環境的接口:隱藏一些數據和方法,公開方便的函數,等等。這將破壞內部機制,並且不管如何都不是良好的面向對象實踐。
如今嘗試使用protocol buffer 類吧。第一件事,但願通信簿能夠把我的的信息詳情寫道通信簿文件中。爲了作到這一點,您須要建立和填入protocol buffer的類實例,並將它們寫入一個輸出流中。
這裏有一個程序,從一個文件中讀取了一個AddressBook,根據用戶的輸入還添加了一個新的Person到通信簿中,並將新的AddressBook從新寫入文件中。
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.addPhones(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.addPeople(
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();
}
}
複製代碼
固然,若是您不能從通信簿中就獲取任何信息,那也就沒什麼用了。這個示例展現了讀取前面一個示例建立的文件並輸出所有信息:
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.getPeopleList()) {
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.getPhonesList()) {
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);
}
}
複製代碼
當您使用protocol buffer發佈代碼後,毋庸置疑,您會但願改善protocol buffer的定義。若是您想您的新buffer能夠向後兼容,而且舊的buffer能夠向前兼容——您確定想要這個——這須要遵循一些規則。在新版本的protocol buffer中須要遵循:
若是您遵循這些規則,舊代碼能夠友好地讀取新的message,而且忽略新的字段。對於舊代碼,那些刪除地optional字段會使用它們默認的值,而repeated字段則爲空。 新代碼顯然會讀取舊的message。不論如何,切記新的optional字段在舊的message中是不會出現的,因此您須要檢查它們是否設置了值,可使用 has_,或者在您的 .proto文件中在該字段的tab編號後面使用 [default = value] 爲其提供一個default值。 若是optional字段沒有指定default值,則會根據該類型自動爲其賦值:string類型則賦值空串,對於布爾類型則賦值爲false,對於數值類型則賦值爲0. 請注意,假如您添加了一個repeated字段,您的新代碼將無從知曉該字段是空的(新代碼),仍是歷來沒有設置過值(舊代碼),由於它沒有 has_ 方法。
Protocol buffers 不只僅只是提供了簡單的訪問和序列化功能。能夠訪問 Java API幫助文檔.
protocol message類體哦概念股了一個關鍵的特性——反射。您能夠遍歷message的全部字段和操做它們的值而不須要編寫指定的message類型的代碼。 反射的一個有用的使用方式是在各類編碼之間轉換協議消息,例如XML或者JSON。 一個關於反射的更高級的用法是從兩個相同類型的消息message中找出不一樣之處,或者開發出一種協議消息的「正則表達式」,是的您能夠編寫這種表達式去匹配某些message內容。 若是您充分發揮您的想象,使用protocol Buffers能夠應用在更普遍的問題上,正如您所指望的那樣。