天天看點

Protocol Buffers使用入門 1. 前言 2. 為什麼使用Protocol Buffers 3. 定義協定格式 4. 編譯Protocol Buffers  5. Protocol Buffer Java API 6. 使用PB生成類寫入  7. 使用PB生成類讀取 8. 拓展PB 9. 進階用法

轉自:http://shitouer.cn/2013/04/google-protocol-buffers-tutorial/

1. 前言

這篇入門教程是基于Java語言的,這篇文章我們将會:

  1. 建立一個.proto檔案,在其内定義一些PB message
  2. 使用PB編譯器
  3. 使用PB Java API 讀寫資料

這篇文章僅是入門手冊,如果想深入學習及了解,可以參看: Protocol Buffer Language Guide, Java API Reference, Java Generated Code Guide, 以及Encoding Reference。

2. 為什麼使用Protocol Buffers

接下來用“通訊簿”這樣一個非常簡單的應用來舉例。該應用能夠寫入并讀取“聯系人”資訊,每個聯系人由name,ID,email address以及contact photo number組成。這些資訊的最終存儲在檔案中。

如何序列化并檢索這樣的結構化資料呢?有以下解決方案:

  1.  使用Java序列化(Java Serialization)。這是最直接的解決方式,因為該方式是内置于Java語言的,但是,這種方式有許多問題(Effective Java 對此有詳細介紹),而且當有其他應用程式(比如C++ 程式及Python程式書寫的應用)與之共享資料的時候,這種方式就不能工作了。
  2. 将資料項編碼成一種特殊的字元串。例如将四個整數編碼成“12:3:-23:67”。這種方法簡單且靈活,但是卻需要編寫獨立的,隻需要用一次的編碼和解碼代碼,并且解析過程需要一些運作成本。這種方式對于簡單的資料結構非常有效。
  3. 将資料序列化為XML。這種方式非常誘人,因為易于閱讀(某種程度上)并且有不同語言的多種解析庫。在需要與其他應用或者項目共享資料的時候,這是一種非常有效的方式。但是,XML是出了名的耗空間,在編碼解碼上會有很大的性能損耗。而且呢,操作XML DOM數非常的複雜,遠不如操作類中的字段簡單。

Protocol Buffers可以靈活,高效且自動化的解決該問題,隻需要:

  1. 建立一個.proto 檔案,描述希望資料存儲結構
  2. 使用PB compiler 建立一個類,該類可以高效的,以二進制方式自動編碼和解析PB資料

該生成類提供組成PB資料字段的getter和setter方法,甚至考慮了如何高效的讀寫PB資料。更厲害的是,PB友好的支援字段拓展,拓展後的代碼,依然能夠正确的讀取原來格式編碼的資料。

3. 定義協定格式

首先需要建立一個.proto檔案。非常簡單,每一個需要序列化的資料結構,編碼一個PB message,然後為message中的字段指明一個名字和類型即可。該“通訊簿”的.proto 檔案addressbook.proto定義如下:

?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

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;

}

可以看到,文法非常類似Java或者C++,接下來,我們一條一條來過一遍每句話的含義:

  • .proto檔案以一個package聲明開始。該聲明有助于避免不同項目建設的命名沖突。Java版的PB,在沒有指明java_package的情況下,生成的類預設的package即為此package。這裡我們生命的java_package,是以最終生成的類會位于com.example.tutorial package下。這裡需要強調一下,即使指明了java_package,我們建議依舊定義.proto檔案的package。
  • 在package聲明之後,緊接着是專門為java指定的兩個選項:java_package 以及 java_outer_classname。java_package我們已經說過,不再贅述。java_outer_classname為生成類的名字,該類包含了所有在.proto中定義的類。如果該選項不顯式指明的話,會按照駝峰規則,将.proto檔案的名字作為該類名。例如“addressbook.proto”将會是“Addressbook”,“address_book.proto”即為“AddressBook”
  • java指定選項後邊,即為message定義。每個message是一個包含了一系列指明了類型的字段的集合。這裡的字段類型包含大多數的标準簡單資料類型,包括bool,int32,float,double以及string。Message中也可以定義嵌套的message,例如“Person” message 包含“PhoneNumber” message。也可以将已定義的message作為新的資料類型,例如上例中,PhoneNumber類型在Person内部定義,但他是phone的type。在需要一個字段包含預先定義的一個清單的時候,也可以定義枚舉類型,例如“PhoneType”。
  • 我們注意到, 每一個message中的字段,都有“=1”,“=2”這樣的标記,這可不是初始化指派,該值是message中,該字段的唯一标示符,在二進制編碼時候會用到。數字1~15的表示需求少于一個位元組,是以在編碼的時候,有這樣一個優化,你可以用1~15标記最常使用或者重複字段元素(repeated elements)。用16或者更大的數字來标記不太常用的可選元素。再重複字段中,每一個元素都需重複編碼标簽數字,是以,該優化對重複字段最佳(repeat fileds)。

message的沒一個字段,都要用如下的三個修飾符(modifier)來聲明:

  1. required:必須指派,不能為空,否則該條message會被認為是“uninitialized”。build一個“uninitialized” message會抛出一個RuntimeException異常,解析一條“uninitialized” message會抛出一條IOException異常。除此之外,“required”字段跟“optional”字段并無差别。
  2. optional:字段可以指派,也可以不指派。假如沒有指派的話,會被賦上預設值。對于簡單類型,預設值可以自己設定,例如上例的PhoneNumber中的PhoneType字段。如果沒有自行設定,會被賦上一個系統預設值,數字類型會被賦為0,String類型會被賦為空字元串,bool類型會被賦為false。對于内置的message,預設值為該message的預設執行個體或者原型,即其内所有字段均為設定。當擷取沒有顯式設定值的optional字段的值時,就會傳回該字段的預設值。
  3. repeated:該字段可以重複任意次數,包括0次。重複資料的順序将會儲存在protocol buffer中,将這個字段想象成一個可以自動設定size的數組就可以了。

 Notice:應該格外小心定義Required字段。當因為某原因要把Required字段改為Optional字段是,會有問題,老版本讀取器會認為消息中沒有該字段不完整,可能會拒絕或者丢棄該字段(Google文檔是這麼說的,但是我試了一下,将required的改為optional的,再用原來required時候的解析代碼去讀,如果字段指派的話,并不會出錯,但是如果字段未指派,會報這樣錯誤:Exception in thread “main” com.google.protobuf.InvalidProtocolBufferException: Message missing required fields:fieldname)。在設計時,盡量将這種驗證放在應用程式端的完成。Google的一些工程師對此也很困惑,他們覺得,required類型壞處大于好處,應該盡量僅适用optional或者repeated的。但也并不是所有的人都這麼想。

如果想深入學習.proto檔案書寫,可以參考Protocol Buffer Language Guide。但是不要妄想會有類似于類繼承這樣的機制,Protocol Buffers不做這個…

4. 編譯Protocol Buffers

定義好.proto檔案後,接下來,就是使用該檔案,運作PB的編譯器protoc,編譯.proto檔案,生成相關類,可以使用這些類讀寫“通訊簿”沒得message。接下來我們要做:

  1. 如果你還沒有安裝PB編譯器,到這裡現在安裝:download the package
  2. 安裝後,運作protoc,結束後會發現在項目com.example.tutorial package下,生成了AddressBookProtos.java檔案:

?

1 2 3

protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto

#for example

protoc -I=G:\workspace\protobuf\message --java_out=G:\workspace\protobuf\src\main\java G:\workspace\protobuf\messages\addressbook.proto

  • -I:指明應用程式的源碼位置,假如不指派,則有目前路徑(說實話,該處我是直譯了,并不明白是什麼意思。我做了嘗試,該值不能為空,如果為空,則提示賦了一個空檔案夾,如果是目前路徑,請用.代替,我用.代替,又提示不對。但是可以是任何一個路徑,都運作正确,隻要不為空);
  • –java_out:指明目的路徑,即生成代碼輸出路徑。因為我們這裡是基于java來說的,是以這裡是–java_out,相對其他語言,設定為相對語言即可
  • 最後一個參數即.proto檔案

Notice:此處運作完畢後,檢視生成的代碼,很有可能會出現一些類沒有定義等錯誤,例如:com.google cannot be resolved to a type等。這是因為項目中缺少protocol buffers的相應library。在Protocol Buffers的源碼包裡,你會發現java/src/main/java,将這下邊的檔案拷貝到你的項目,大概可以解決問題。我隻能說大概,因為當時我在弄得時候,也是剛學,各種出錯,比較惡心。有一個簡單的方法,呵呵,對于懶漢來說。建立一個maven的java項目,在pom.xml中,添加Protocol Buffers的依賴即可解決所有問題~在pom.xml中添加如下依賴(注意版本):

?

1 2 3 4 5

<

dependency

>

<

groupId

>com.google.protobuf</

groupId

>

<

artifactId

>protobuf-java</

artifactId

>

<

version

>2.5.0</

version

>

</

dependency

>

 5. Protocol Buffer Java API

5.1 産生的類及方法

接下來看一下PB編譯器建立了那些類以及方法。首先會發現一個.java檔案,其内部定義了一個AddressBookProtos類,即我們在addressbook.proto檔案java_outer_classname 指定的。該類内部有一系列内部類,對應分别是我們在addressbook.proto中定義的message。每個類内部都有相應的Builder類,我們可以用它建立類的執行個體。生成的類及類内部的Builder類,均自動生成了擷取message中字段的方法,不同的是,生成的類僅有getter方法,而生成類内部的Builder既有getter方法,又有setter方法。本例中Person類,其僅有getter方法,如圖所示:

Protocol Buffers使用入門 1. 前言 2. 為什麼使用Protocol Buffers 3. 定義協定格式 4. 編譯Protocol Buffers  5. Protocol Buffer Java API 6. 使用PB生成類寫入  7. 使用PB生成類讀取 8. 拓展PB 9. 進階用法

 但是Person.Builder類,既有getter方法,又有setter方法,如圖:

Protocol Buffers使用入門 1. 前言 2. 為什麼使用Protocol Buffers 3. 定義協定格式 4. 編譯Protocol Buffers  5. Protocol Buffer Java API 6. 使用PB生成類寫入  7. 使用PB生成類讀取 8. 拓展PB 9. 進階用法

person.builder

從上邊兩張圖可以看到:

  1. 每一個字段都有JavaBean風格的getter和setter
  2. 對于每一個簡單類型變量,還對應都有一個has這樣的一個方法,如果該字段被指派了,則傳回true,否則,傳回false
  3. 對每一個變量,都有一個clear方法,用于置空字段

對于repeated字段:

Protocol Buffers使用入門 1. 前言 2. 為什麼使用Protocol Buffers 3. 定義協定格式 4. 編譯Protocol Buffers  5. Protocol Buffer Java API 6. 使用PB生成類寫入  7. 使用PB生成類讀取 8. 拓展PB 9. 進階用法

repeated filed

從圖上看:

  1. 從person.builder圖上看出,對于repeated字段,還有一個特殊的getter,即getPhoneCount方法,及repeated字段還有一個特殊的count方法
  2. 其getter和setter方法根據index擷取或設定一個資料項
  3. add()方法用于附加一個資料項
  4. addAll()方法來直接增加一個容器中的所有資料項

注意到一點:所有的這些方法均命名均符合駝峰規則,即使在.proto檔案中是小寫的。PB compiler生成的方法及字段等都是按照駝峰規則來産生,以符合基本的Java規範,當然,其他語言也盡量如此。是以,在proto檔案中,命名最好使用用“_”來分割不同小寫的單詞。

 5.2 枚舉及嵌套類

從代碼中可以發現,還産生了一個枚舉:PhoneType,該枚舉位于Person類内部:

?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

public

enum

PhoneType

implements

com.google.protobuf.ProtocolMessageEnum {

MOBILE(

,

),

HOME(

1

,

1

),

WORK(

2

,

2

),

;

...

}

除此之外,如我們所預料,還有一個Person.PhoneNumber内部類,嵌套在Person類中,可以自行看一下生成代碼,不再粘貼。

5.3 Builders vs. Messages

由PB compiler生成的消息類是不可變的。一旦一個消息對象建構出來,他就不再能夠修改,就像java中的String一樣。在建構一個message之前,首先要建構一個builder,然後使用builder的setter或者add()等方法為所需字段指派,之後調用builder對象的build方法。

在使用中會發現,這些構造message對象的builder的方法,都又會傳回一個新的builder,事實上,該builder跟調用這個方法的builder是同一方法。這樣做的目的,僅是為了友善而已,我們可以把所有的setter寫在一行内。

如下構造一個Person執行個體:

?

1 2 3 4 5 6 7 8 9 10 11 12

Person john = Person

.newBuilder()

.setId(

1

)

.setName(

"john"

)

.setEmail(

"[email protected]"

)

.addPhone(

PhoneNumber

.newBuilder()

.setNumber(

"1861xxxxxxx"

)

.setType(PhoneType.WORK)

.build()

).build();

5.4 标準消息方法

每一個消息類及Builder類,基本都包含一些公用方法,用來檢查和維護這個message,包括:

  1.  isInitialized(): 檢查是否所有的required字段是否被指派
  2. toString(): 傳回一個便于閱讀的message表示(本來是二進制的,不可讀),尤其在debug時候比較有用
  3. mergeFrom(Message other): 僅builder有此方法,将其message的内容與此message合并,覆寫簡單及重複字段
  4. clear(): 僅builder有此方法,清空所有的字段

5.5 解析及序列化

對于每一個PB類,均提供了讀寫二進制資料的方法:

  1. byte[] toByteArray();: 序列化message并且傳回一個原始位元組類型的位元組數組
  2. static Person parseFrom(byte[] data);: 将給定的位元組數組解析為message
  3. void writeTo(OutputStream output);: 将序列化後的message寫入到輸出流
  4. static Person parseFrom(InputStream input);: 讀入并且将輸入流解析為一個message

這裡僅列出了幾個解析及序列化方法,完整清單,可以參見:

Message

 API reference

6. 使用PB生成類寫入

接下來使用這些生成的PB類,初始化一些聯系人,并将其寫入一個檔案中。

下面的程式首先從一個檔案中讀取一個通訊簿(AddressBook),然後添加一個新的聯系人,再将新的通訊簿寫回到檔案。

?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87

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() >

) {

person.setEmail(email);

}

while

(

true

) {

stdout.print(

"Enter a phone number (or leave blank to finish): "

);

String number = stdin.readLine();

if

(number.length() ==

) {

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[

]));

}

catch

(FileNotFoundException e) {

System.out.println(args[

]

+

": 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[

]);

addressBook.build().writeTo(output);

output.close();

}

}

 7. 使用PB生成類讀取

運作第六部分程式,寫入幾個聯系人到檔案中,接下來,我們就要讀取聯系人。程式入下:

?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

package

com.example.tutorial;

import

java.io.FileInputStream;

import

com.example.tutorial.AddressBookProtos.AddressBook;

import

com.example.tutorial.AddressBookProtos.Person;

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[

]));

Print(addressBook);

}

}

至此我們已經可以使用生成類寫入和讀取PB message。

8. 拓展PB

當産品釋出後,遲早有一天我們需要改善我們的PB定義。如果要做到新的PB能夠向後相容,同時老的PB又能夠向前相容,我們必須遵守如下規則:

  1. 千萬不要修改現有字段後邊的數值标簽
  2. 千萬不要增加或者删除required字段
  3. 可以删除optional或者repeated字段
  4. 可以添加新的optional或者repeated字段,但是必須使用新的數字标簽(該數字标簽必須從未在該PB中使用過,包括已經删除字段的數字标簽)

如果違反了這些規則,會有一些相應的異常,可參見some exceptions,但是這些異常,很少很少會被用到。

遵守這些規則,老的代碼可以正确的讀取新的message,但是會忽略新的字段;對于删掉的optional的字段,老代碼會使用他們的預設值;對于删除的repeated字段,則把他們置為空。

新的代碼也将能夠透明的讀取老的messages。但是必須注意,新的optional字段在老的message中是不存在的,必須顯式的使用has_方法來判斷其是否設定了,或者在.proto 檔案中以[default = value]形式提供預設值。如果沒有指定預設值的話,會按照類型預設值指派。對于string類型,預設值是空字元串。對于bool來說,預設值是false。對于數字類型,預設值是0。

9. 進階用法

Protocol Buffers的應用遠遠不止簡單的存取以及序列化。如果想了解更多用法,可以去研究Java API reference。

Protocol Message Class提供了一個重要特性:反射。不需要再寫任何特殊的message類型就可以周遊一條message的所有字段以及操作字段的值。反射的一個非常重要的應用是可以将PBmessage與其他的編碼語言進行轉化,例如與XML或者JSON之間。

反射另外一個更加進階的應用應該是兩個同一類型message的之間的不同,或者開發一種可以成為“Protocol Buffers 正規表達式”的應用,使用它,可以編寫符合一定消息内容的表達式。

除此之外,開動腦筋,你會發現,Protocol Buffers能解決遠遠超過你剛開始對他的期待。

推薦閱讀順序,希望給你帶來收獲~