Protobuf的那些事

當大多數公司還在糾結於如何更好提高MobileAPI的性能時,有的公司已經開始拋棄http + json,開始走向tcp + protobuf的路線了。

那麼什麼是protobuf呢protobuf是一種基於二進制的協議,它能夠非常快速高效的序列化數據,考慮之前的xml,但是它的體積要比xml小的多得多。當然使用它還有其他非常之多的理由。我們考慮java的情況,傳統的序列化方法就是實現Serialization,不過它帶來非常之多的煩惱,比如版本兼容,這會帶來非常之大的測試壓力,且它的處理完的數據並不能跨語言的分享啊,python or c++都不能夠使用,這是很頭疼的。除此之外,我們序列化數據的時候我們會使用一些ad-hoc way,比如會把數據都放在一個字符串裏面,考慮有四個整形數據的例子:"12:3:-23:67",它顯然是足夠簡單的,但是卻需要手寫一些解析,編碼代碼,並且,還有一些運行時損耗。再後者,我們可能會想到使用xml,顯然它是不太好的,不然也就沒有json了。

綜合以上,便是protobuf誕生的理由,現在我們要開始正式學習它了。


在使用之前,我們需要安裝它的庫,編譯器啥的,轉到github :https://github.com/google/protobuf,按照readme裏面下載安裝就行了,之後安裝Java的版本,如果一切都正常的話 在終端裏面輸入protoc --version會看到當前protoc編譯器的版本號。


那再用java來使用protobuf的時候,我們需要三個步驟:

1:定義.proto文件,用來定義你的數據結構

2:使用protoc編譯器進行編譯,導出java代碼

3:使用java api進行數據的讀寫


我這裏考慮創建一個 AddressBook的例子

在這個類中我們存放聯繫簿,練習簿中有你記錄的聯繫人以及他的個人信息,包括手機號碼,姓名啥的

我們先粗略的看下proto文件內容:






是不是似曾相識的感覺,他和c++,java代碼還是很像的。我們逐行分析代碼

第一行是指定版本號,如果沒有,大概編譯器會報如下錯誤:

[libprotobuf WARNING google/protobuf/compiler/parser.cc:547] No syntax specified for the proto file. Please use 'syntax = "proto2";' or 'syntax = "proto3";' to specify a syntax version. (Defaulted to proto2 syntax.

注意,它必須放在第一行哦,在下面是包名,他是用來防止Protocol Buffers name space裏的名字衝突,所以還是不省略爲好,再往下是生成java文件後的包名,很顯然,第五行的便是類名了

如果你真的懶得寫這些的話 關於他們缺省值你可以參考官網給出的解釋:


再往下,你可以看到很多關於message的定義,他是用來存放數據域的,數據域有自己的類型,protobuf支持的類型有bool, int32, float, double, and string,包括自定義類型(也爲message)等。比如Person有自己的名字,id,email,和聯繫電話,觀之name,是string類型的,前面還有 required修飾符,表示這個域必須有,後面還有 = 1,這個符號,這是用來在二進制編碼裏面唯一確認當前的域的,值得注意的是1-15要求更少得存儲空間去存儲這個標記數字,也就是說,如果你某些域用的很頻繁,應該首先分配這些數字,這也是作爲優化的一種方式。

往下面讀,可以看到email是optional,也就是可選的,這個域可有可無,如果沒有設置的話,系統會自動填充一個合理地值,比如對於bool 你可以是false,字符串給定一個空值,所以,如果你對編譯器沒有信心,你可以自己給定一個默認的值,考慮第二十行的做法

對於PhoneType,他是一個枚舉類型,枚舉類型裏面也要給定標記數字(Tag number)

通常情況下,一個人不可能只擁有一個號碼,所以phone應該是一個集合,這個集合可以是變化的,可爲空,你可以想象爲一個動態變化的數組,所以它的修飾詞是repeated

總結下:

一個域由以下部分組成:

修飾詞 類型  域名  = 標記值

到目前爲止,所有的基礎內容已經介紹完,然後我們保存下文件,後綴名爲.proto


切換到命令行模式:輸入 

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

-I指定源目的文件夾,一般是你的應用源碼位置

--java_out 是生成代碼的目的地 一般和src dir相同

後面一個參數是proto文件的位置


具體的例子:

protoc -I=./ --java_out=./ ./first.proto


便可以在你指定的目標文件夾下面看到生成的Java文件了 :)


The Protocol Buffer API

我們看下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();

可以看到,Builder比Person有更“強的”方法。 每個 clear方法用於重置域到它的初始狀態

重複域有count方法,用於返回這個域的大小


Enums and Nested Classes:

在Person中,我們定義了一個枚舉類型,下面是枚舉類型的Java代碼:

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

Builders vs. Messages:

builder使用方式:

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使用方式:

WRITE:

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

READ:

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

Extending a Protocol Buffer:

如果以後你想升級Proto文件定義,請遵守一下條款:

1:不可以修改現有域的任何標識數字(tag number)

2:不可以添加刪除任何required域

3:你可以刪除optional,repeated域

4:你可以添加optional repeated域,但是你必須要用不同的tag number(之前刪除域的tag number也不能使用!)

protobuf github 包下載: http://download.csdn.net/detail/u013022222/9405304

點我

轉載請註明出處:

http://blog.csdn.net/u013022222/article/details/50521835

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章