Google Protocol Buffers 入門


個人小站,正在持續整理中,歡迎訪問:http://shitouer.cn

小站博文地址:Google Protocol Buffers 入門

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

Google Protocol Buffers 概述

Google Protocol Buffers 入門

Protocol Buffers 語法指南

Google Protocol Buffers 編碼(Encoding)

1. 前言

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

  1. 創建一個.proto文件,在其內定義一些PB message
  2. 使用PB編譯器
  3. 使用PB Java API 讀寫數據

這篇文章僅是入門手冊,如果想深入學習及了解,可以參看: Protocol Buffer Language GuideJava API ReferenceJava 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定義如下:

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文件:
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中添加如下依賴(注意版本):

<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方法,如圖所示:

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

person.builder
person.builder

從上邊兩張圖可以看到:

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

對於repeated字段:

repeated filed
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類內部:

 public enum PhoneType
        implements com.google.protobuf.ProtocolMessageEnum {
      /**
       * <code>MOBILE = 0;</code>
       */
      MOBILE(0, 0),
      /**
       * <code>HOME = 1;</code>
       */
      HOME(1, 1),
      /**
       * <code>WORK = 2;</code>
       */
      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實例:

Person john = Person
		.newBuilder()
		.setId(1)
		.setName("john")
		.setEmail("john@youku.com")
		.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),然后添加一個新的聯系人,再將新的通訊簿寫回到文件。

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

 7. 使用PB生成類讀取

運行第六部分程序,寫入幾個聯系人到文件中,接下來,我們就要讀取聯系人。程序入下:

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[0]));

    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能解決遠遠超過你剛開始對他的期待。

譯自:https://developers.google.com/protocol-buffers/docs/javatutorial

說實話,翻譯下來整個文章非常辛苦,而且都要敲代碼去親自試驗能否通過,所以如果您想轉載,非常歡迎,但請注明出處,也算是對俺辛苦的尊重~ 

原創作品,允許轉載,轉載時請務必以超鏈接形式標明文章 原始出處 、作者信息和 本聲明。否則將追究法律責任。http://shitouer.cn/2013/04/google-protocol-buffers-tutorial/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM