Protobuf -java基礎教程(譯文)

protobuf 基礎教程

最近忽然對RPC序列化感興趣,可是發現Protobuf的資料並很少,因而在官網找到了Java使用Protocol Buffer的入門指南,用蹩腳的英文翻譯了下,以饗同道。原文地址html

示例開始:定義協議格式 Protocol Format

示例:一個簡單的通信簿, .proto 文件見 addressbook.protojava

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_packagejava_outer_classnamejava_package 指定了生成的java類須要放在哪一個包下面,若是沒有指定這個值,則會使用package指定的值。 java_outer_classname 選項定義了類名,包含這個.proto文件裏面的全部類,若是沒有明確指定這個值的話,將會把文件名經過駝峯大小寫命名方式做爲類名, 例如 my_proto.proto 則默認會生成 MyProto 類名。正則表達式

接下來,就有了消息(message)定義。編程

一個消息(message)是包含一系列類型字段的聚合。數組

不少標準的簡單數據類型供字段可用,包含:maven

  • bool
  • int32
  • float
  • double
  • string

您還能夠添加其餘的結構類型爲您的消息字段類型所用。前面的示例中,Person 消息包含了PhoneNumber消息,而AddressBook消息包含Person消息。 您還能夠定義枚舉類型enum,若是你想你的字段可能的值爲一個事先預約好的列表中的值,就可使用enum類型。 這個電話簿示例中,phone number有三種類型: MOBILEHOMEWORK編程語言

=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弊大於利。他們更傾向於使用opyionalrepeated。無論怎樣,這種觀點並不廣泛。

您還能夠在Protocol Buffer 語言指南中瞭解到完整地教程。 不要嘗試去尋找相似繼承的工具,protocol buffer不支持這樣作。

編譯你的Protocol Buffers

如今您有了一個 .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 運行時安裝

Protobuf 支持幾種不一樣的編程語言。針對於每一種語言,你能夠參考源碼中的各類語言的說明指導。

  • 如今運行編譯器,須要指明源目錄(應用源代碼所在-若是您沒有提供一個值的話默認使用當前目錄)、目標目錄(您想要代碼生成的目的目錄,一般相似於 $SRC_DIR)、還有 .proto 的路徑。在這種狀況下,您能夠以下操做:

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

由於您想生成Java類,您看到 --java_out 選項,相似的選項也提供了其餘編程語言。

這將會在您指定的目標目錄生成com/example/tutorial/AddressBookProtos.java

Protocol Buffer API

讓咱們來看看一些生成的代碼,並查看一些由編譯器爲您建立的類與方法。若是您看一下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 字段由一些額外的方法:

  • getXXXCount 方法用於獲取淚飆大小;
  • 增長了根據元素索引下標獲取元素的get、set方法(public PhoneNumber getPhones(int index); 和 public Builder setPhones(int index, PhoneNumber value);)。
  • *add、addAll 方法追加新元素(列表)到列表中。

注意到全部這些訪問方法都使用了駝峯命名方式,即便 .proto 文件使用了小寫和下劃線。這個轉換是由protocol編譯器自動完成的,因此生成的類符合Java風格標準規範。在 .proto 中您應該老是將字段名用小寫和下劃線來命名。 能夠參考風格指南獲取更多良好的 .proto 命名風格。 更多關於編譯器爲字段生成的特定的詳細信息能夠參考 Java生成代碼參考指南

枚舉和內部類

生成的代碼包含了一個枚舉 PhoneType ,嵌套在 Person 類中:

public static enum PhoneType {
  MOBILE(0, 0),
  HOME(1, 1),
  WORK(2, 2),
  ;
  ...
}
複製代碼

內部類Person.PhoneNumber也生成了,正如您指望的同樣,它是做爲Person的一個內部類的。

Builders vs. Messages

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方法

每一個message和builder類也包含一系列其餘的方法,可讓您檢查與操做message:

  • isInitialized() : 檢查是否全部的required 字段都已經set過值了/
  • toString() : 返回一個可讀性良好的message表示,一般對調試特別有用。
  • mergeFrom(Message other) : (只在builder有)將other合併到該message中
  • clear() : (只在builder有)清除全部的字段,時其恢復到最初的空狀態

解析和序列化

最後,每一個 proto buffer 類都有一些用於讀和寫二進制的方法。

  • byte[] toByteArray() : 序列化message,返回一個byte數組。
  • static Person parseFrom(byte[] data) : 將給定的byte數組解析成一個message。
  • void writeTo(OutputStream output) : 序列化message,並將其寫入一個輸出流。
  • static Person parseFrom(InputStream input) : 讀取一個輸入流並從其解析處一個message。

這些只是爲序列化和解析(反序列化)而提供的兩組操做方法。更多完整列表能夠參考 Message API幫助文檔

Protocol Buffers 和 面向對象 Protocol buffer 類基本上是啞數據持有者(相似C中的struct);在對象模型中,它們不是一等公民。若是您想向生成類中添加更豐富的行爲,最好的方式就是在一個特定於應用程序中的類中包裝protocol buffer生成的類。若是您不能控制.proto文件的設計(例如,若是您正在重用來自另外一個項目的一個文件),那麼包裝協議緩衝區也是一個好主意。在這種狀況下,您可使用包裝器類來建立更適合您的應用程序的獨特環境的接口:隱藏一些數據和方法,公開方便的函數,等等。這將破壞內部機制,並且不管如何都不是良好的面向對象實踐。

寫一個Message

如今嘗試使用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();
  }
}
複製代碼

讀一個Message

固然,若是您不能從通信簿中就獲取任何信息,那也就沒什麼用了。這個示例展現了讀取前面一個示例建立的文件並輸出所有信息:

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發佈代碼後,毋庸置疑,您會但願改善protocol buffer的定義。若是您想您的新buffer能夠向後兼容,而且舊的buffer能夠向前兼容——您確定想要這個——這須要遵循一些規則。在新版本的protocol buffer中須要遵循:

  • 您不能改變已經存在的字段的tag的編號
  • 您不能添加或者刪除任何required字段
  • 您能夠刪除optional或者repeated字段
  • 您能夠添加新的optional或者repeated字段,但那時您必須使用新的tag編號(即該tag編號沒有在這個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能夠應用在更普遍的問題上,正如您所指望的那樣。

相關文章
相關標籤/搜索