Google Protocol Buffers簡介

什麼是 protocol buffers ?

Protocol buffers 是一種靈活、高效的序列化結構數據的自動機制--想一想XML,可是它更小,更快,更簡單。你只須要把你須要怎樣結構化你的數據定義一次,你就能使用特殊生成的代碼來方便的用多種語言從一系列數據流中讀寫你的結構化數據。你甚至不須要中斷你用"老"結構編譯好的已經部署的程序來更新你的數據結構。html

 

它是怎樣工做的?

你在一個名爲.proto的文件裏用protocol buffer message 定義你須要序列化數據的結構。每一個protocol buffer message 是一個小的信息邏輯記錄,包含了一系列的name-value對。這裏有一個簡單的.proto例子,它定義了一個person的信息。java

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 type 都有一個或多個獨特的屬性,每一個屬性都有一個name和一個value類型,value類型能夠是numbers ( 整數或浮點數),booleans,strings,raw bytes或者其餘protocol buffer message types,容許你以嵌套結構組織你的結構。你能夠指定optional,required、和repeated屬性。你能夠從 Protocol Buffer Language Guide找到更多的關於如何寫.proto文件的信息。python

一旦你定義了你的信息,你就能夠運行protocol buffer 編譯器來編譯你的.proto文件來生成特定語言的數據訪問類。這些類提供了簡單的對屬性的訪問函數(例如 name()set_name() )和用來序列化整個結構到raw bytes和從raw bytes 解析出結構的函數。例如,假如你使用的是c++語言,用編譯器編譯上面那個person.proto文件會生成一個Person類。你能夠在你的應用裏用這個類來操縱Person類的對象。好比,你可能會寫一些這樣的代碼:ios

Person person;
person.set_name("John Doe");
person.set_id(1234);
person.set_email("jdoe@example.com");
fstream output("myfile", ios::out | ios::binary);
person.SerializeToOstream(&output);

而後,你能夠經過這樣的代碼來把你的message讀進來:c++

fstream input("myfile", ios::in | ios::binary);
Person person;
person.ParseFromIstream(&input);
cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;

你能夠給你的message添加新的屬性而不打破向後兼容性(backwards-compatibility);舊的二進制文件僅僅在編譯的時候忽略那些新的屬性。這樣一來,若是你有一個通訊協議使用了protocol buffers當作它傳輸的數據格式,你能夠擴展你的通訊協議而不用擔憂破壞現有的代碼。編程

你能夠在API Reference section找到完整的文檔,而且你能夠在Protocol buffer encoding找出關於protocol buffer 編碼的更多信息.api

 

爲何不用XML等其餘技術?

Protocol buffers相對XML在序列化數據的時候有不少優點。protocol buffers :數組

  • 更簡單
  • 比XML小3到10倍
  • 比XML快20到100倍
  • 更少歧義
  • 能夠生成方便編程的數據訪問類

例如,倘若你要給person建模,它有nameemail屬性。在XML裏,你須要:數據結構

<person>
    <name>John Doe</name>
    <email>jdoe@example.com</email>
</person>

用protocol buffer message(在protocol buffer 的text format)是這樣的app

# Textual representation of a protocol buffer.
# This is *not* the binary format used on the wire.
person {
  name: "John Doe"
  email: "jdoe@example.com"
}

當上面這段代碼被編譯成binary format(上面那段text format只是爲了方便人類讀寫編輯的)的時候,它可能只佔28字節長,僅僅須要100~200納秒就能編譯。那個XML版本即便移除全部空白也至少須要69字節,而且須要5000~10000納秒來編譯。

一樣,操做protocol buffer 更容易:

cout << "Name: " << person.name() << endl;
cout << "E-mail:" << person.email() << endl;

然而若是使用XML你須要這樣作:

cout << "Name: "
       << person.getElementsByTagName("name")->item(0)->innerText()
       << endl;
cout << "E-mail: "
       << person.getElementsByTagName("email")->item(0)->innerText()
       << endl;

嗯~聽起來能解決個人問題!我該怎樣開始呢?

下載地址--這個包包含了完整的c++,python和java語言的編譯器源代碼,和I/O和測試的類。安裝請參閱README。

一旦你安裝好了,就能夠跟着入門教程來學習了。

入門教程c++版

這個教程會帶你走一遍使用protocol buffer的流程,建立一個簡單的實例程序,學會基本的使用方法:

  • .proto文件裏定義信息格式
  • 使用protocol buffer編譯器
  • 使用c++ protocol buffer API來讀寫信息

爲何使用protocol buffers?

在這個教程裏咱們要建立一個簡單的「地址簿」程序來在文件裏讀寫人們的聯繫人信息。每一個人都有一個name,id,email address和一個聯繫電話。

你怎樣序列化和讀取這樣一個結構數據呢?這裏有三種方法:

  • 內存中原始的字節數據結構能夠存儲爲2進制形式。這種方法很脆弱,由於讀取代碼必須用一樣的內存佈局編譯,還要考慮使用相同的內存大小端等等。當文件積累了不少數據以後,拷貝處處都是,擴展結構就很困難了。
  • 你能夠發明一個點對點的方式來把數據編碼爲一個簡單的字符串---例如編碼4個整數爲"12:3:-23:67".這是一個簡單且靈活的方法,儘管它須要你編寫一次性的讀寫代碼,讀取須要一些運行時間。這種方法適用於編碼十分簡單的數據。
  • 序列化數據到XML文件。若是你須要和其餘程序共享數據,那麼這將是個好方法。然而,XML佔內存已經臭名昭著了,解析編碼它會形成程序性能大幅降低。在XML DOM tree裏巡弋也遠比在類裏查找屬性複雜的多。

protocol buffers 靈活高效,能夠解決上述問題。你只須要編寫一個.proto文件來描述你要使用的數據結構。protocol buffer 編譯器能夠把.proto文件編譯成一個相似於ORM(object relation mapping)實現類的數據訪問類,這個類能夠把高效的用二進制文件方式存儲的數據讀寫出來。更多的是,它提供了一種向後兼容的擴展機制,使你能夠不用擔憂兼容性問題來擴展你的數據格式。


 

定義你的protocol Format

爲了建立地址簿程序,你須要首先定義一個.proto文件。定義.proto文件十分簡單: 你添加一個 message 給你想序列化的每一個數據結構 ,而後指定一個 name和一個typemessage的每一個屬性。下面是一個.proto文件,定義了地址簿數據結構,addressbook.proto:

package tutorial;
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;
}

rotobuffer支持的內建數據類型包括bool,int32,float,double,string
注意:message能夠嵌套,好比 PhoneNumber 就定義在Person裏。 
「=1」,「=2」標記了每一個元素的惟一「tag」,這是用在二進制編碼裏的。使用1-15能夠在1個字節裏表示這些tag,節省空間,通常把經常使用的須要大量重複的元素使用1-15來編碼,把16以上的tag留給不經常使用的元素。

每一個屬性必須標記爲下列修飾符之一:

    • required : 故名思議就是必須提供值的屬性,當你把屬性設置爲required的時候要當心,由於若是之後想修改成其餘類型,老的讀取類就不兼容新的數據了。
    • optional: 就是能夠不提供值的屬性,若是沒有提供值,會設置爲默認值。默認值能夠本身提供,若是沒有本身提供默認值,會設置爲系統默認值:numeric類型會置爲0,字符串置爲空串,bool置爲false;對於內嵌類型,默認值永遠是空實例。
    • repeated:就是可能重複任意次(包含0次).重複值的順序會在二進制文件保存下來,能夠把重複的屬性看作動態大小的數組。注意,因爲歷史緣由,repeated數值屬性不能有效的被編碼成二進制,新的代碼可使用[packed=true]來得到更好的編碼效率 

 

例如: repeated int32 samples = 4 [packed=true];

編譯你的protocol buffers文件

protoc -I=\$SRC_DIR --cpp_out=\$DST_DIR \$SRC_DIR/addressbook.proto
  • addressbook.pb.h
  • addressbook.pb.cc

Protocol Buffer API

咱們如今來看一些生成的code是什麼樣的,編譯器爲咱們生成了什麼類和函數呢? 
若是咱們打開tutorial.pb.h,咱們會看到編譯器給咱們在.proto文件裏定義的每個message都生成了一個class,咱們再看Person類,會發現編譯器給message的每一個屬性都生成了getters和setters,例如,對於name,id,email,和phone屬性,咱們能夠找到這些函數:

// name
  inline bool has_name() const;
  inline void clear_name();
  inline const ::std::string& name() const;
  inline void set_name(const ::std::string& value);
  inline void set_name(const char* value);
  inline ::std::string* mutable_name();
  // id
  inline bool has_id() const;
  inline void clear_id();
  inline int32_t id() const;
  inline void set_id(int32_t value);
  // email
  inline bool has_email() const;
  inline void clear_email();
  inline const ::std::string& email() const;
  inline void set_email(const ::std::string& value);
  inline void set_email(const char* value);
  inline ::std::string* mutable_email();
  // phone
  inline int phone_size() const;
  inline void clear_phone();
  inline const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phone() const;
  inline ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phone();
  inline const ::tutorial::Person_PhoneNumber& phone(int index) const;
  inline ::tutorial::Person_PhoneNumber* mutable_phone(int index);
  inline ::tutorial::Person_PhoneNumber* add_phone();

正如你所能看到的那樣,getters和小寫屬性名同樣,setters以set_開頭。還有has_開頭的判斷是否設置了值的函數。還有clear_開頭的函數用於清空設置的值。

不一樣類型的屬性方法不盡相同,例如 id只有基本的getter,setter方法,而name,email等字符串類型的屬性多了一個mutable_開頭的getter,和一個多出來的setter。即便尚未設置email仍然能夠調用mutable_email。它能夠自動初始化爲一個空字符串。

repeated屬性一樣有些特別的方法,例如phone屬性:

  • 能夠查看_size(這我的有多少個電話號碼)
  • 能夠經過index訪問一個特定的值
  • 能夠添加一個新值(經過add_方法)

更多關於編譯器生成函數的信息請參看C++ generated code reference


 

枚舉和嵌套類

生成的代碼包含了一個PhoneType枚舉對應你的.proto文件裏的enum.你能夠經過Person::PhoneType來使用這個枚舉,和它的值Person::MOBILE,Person::HOME,Person::WORK(具體實現很複雜,但咱們不須要了解它)

編譯器一樣生成了一個嵌套類Person::PhoneNumber。若是查看代碼,會發現實際的類是叫作Person_PhoneNumber,可是使用了一個typedef來重命名了它,惟一的區別是當你想在另外一個文件裏前向聲明這個類的時候,必須使用Person_PhoneNumber來前向聲明它。


 

標準 Message方法

每一個message類還包含了一些其餘方法來使你能檢查或者操做整個message,包括:

  • bool IsInitialized() const;: checks if all the required fields have been set.
  • string DebugString() const;: returns a human-readable representation of the message, particularly useful for debugging.
  • void CopyFrom(const Person& from);: overwrites the message with the given message's values.
  • void Clear();: clears all the elements back to the empty state.

 

解析和序列化

最終,每一個protocol buffer class使用讀寫方法來解析和序列化message到二進制文件裏,這些方法包括:

  • bool SerializeToString(string* output) const;: 序列化一個message而且把字節文件存儲到string裏,這裏使用string僅僅是爲了把它當作一個方便的容器.
  • bool ParseFromString(const string& data);: 從指定的string裏解析message
  • bool SerializeToOstream(ostream* output) const;: 把message寫到指定的c++`ostream`裏。
  • bool ParseFromIstream(istream* input);: 從指定的c++istream讀取message

查看Message API獲取更詳細內容.


 

寫一個Message

如今,讓咱們試着使用編譯器爲咱們生成的類。咱們讓咱們的地址簿程序作的第一件事情是把一我的的我的信息寫到地址簿文件裏。咱們須要生成一個該類的實例而後把它寫入到輸出流裏。

這裏有一個實例程序:

#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
// This function fills in a Person message based on user input.
void PromptForAddress(tutorial::Person* person) {
  cout << "Enter person ID number: ";
  int id;
  cin >> id;
  person->set_id(id);
  cin.ignore(256, '\n');
  cout << "Enter name: ";
  getline(cin, *person->mutable_name());
  cout << "Enter email address (blank for none): ";
  string email;
  getline(cin, email);
  if (!email.empty()) {
    person->set_email(email);
  }
  while (true) {
    cout << "Enter a phone number (or leave blank to finish): ";
    string number;
    getline(cin, number);
    if (number.empty()) {
      break;
    }
    tutorial::Person::PhoneNumber* phone_number = person->add_phone();
    phone_number->set_number(number);
    cout << "Is this a mobile, home, or work phone? ";
    string type;
    getline(cin, type);
    if (type == "mobile") {
      phone_number->set_type(tutorial::Person::MOBILE);
    } else if (type == "home") {
      phone_number->set_type(tutorial::Person::HOME);
    } else if (type == "work") {
      phone_number->set_type(tutorial::Person::WORK);
    } else {
      cout << "Unknown phone type.  Using default." << endl;
    }
  }
}
// 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.
int main(int argc, char* argv[]) {
  // Verify that the version of the library that we linked against is
  // compatible with the version of the headers we compiled against.
  GOOGLE_PROTOBUF_VERIFY_VERSION;
  if (argc != 2) {
    cerr << "Usage:  " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
    return -1;
  }
  tutorial::AddressBook address_book;
  {
    // Read the existing address book.
    fstream input(argv[1], ios::in | ios::binary);
    if (!input) {
      cout << argv[1] << ": File not found.  Creating a new file." << endl;
    } else if (!address_book.ParseFromIstream(&input)) {
      cerr << "Failed to parse address book." << endl;
      return -1;
    }
  }
  // Add an address.
  PromptForAddress(address_book.add_person());
  {
    // Write the new address book back to disk.
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!address_book.SerializeToOstream(&output)) {
      cerr << "Failed to write address book." << endl;
      return -1;
    }
  }
  // Optional:  Delete all global objects allocated by libprotobuf.
  google::protobuf::ShutdownProtobufLibrary();
  return 0;
}

注意代碼中的GOOGLE_PROTOBUF_VERIFY_VERSION宏,在使用c++ Protocol Buffer 以前執行這個宏是一個好的習慣(儘管不是強制要求的)。它會驗證你是否連接了正確的庫,防止你連接版本不匹配的庫。

注意代碼中的ShutdownProtobufLibrary(),它會清楚全部protocol buffer libarary分配的全局對象。一般這是不須要的,由於這個進程老是會退出,系統會接管剩下的內存。可是,若是你使用了一個內存泄露檢查工具,好比valgrand之類的,這類工具會要求你把全部分配的內存釋放掉,或者你在寫一個庫文件,這個庫文件會被同一個進程加載和卸載屢次,這兩種狀況你就須要清理全部東西。


 

讀取一個Message

這是一個從二進制文件讀取地址簿的例子:

#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
// Iterates though all people in the AddressBook and prints info about them.
void ListPeople(const tutorial::AddressBook& address_book) {
  for (int i = 0; i < address_book.person_size(); i++) {
    const tutorial::Person& person = address_book.person(i);
    cout << "Person ID: " << person.id() << endl;
    cout << "  Name: " << person.name() << endl;
    if (person.has_email()) {
      cout << "  E-mail address: " << person.email() << endl;
    }
    for (int j = 0; j < person.phone_size(); j++) {
      const tutorial::Person::PhoneNumber& phone_number = person.phone(j);
      switch (phone_number.type()) {
        case tutorial::Person::MOBILE:
          cout << "  Mobile phone #: ";
          break;
        case tutorial::Person::HOME:
          cout << "  Home phone #: ";
          break;
        case tutorial::Person::WORK:
          cout << "  Work phone #: ";
          break;
      }
      cout << phone_number.number() << endl;
    }
  }
}
// Main function:  Reads the entire address book from a file and prints all
//   the information inside.
int main(int argc, char* argv[]) {
  // Verify that the version of the library that we linked against is
  // compatible with the version of the headers we compiled against.
  GOOGLE_PROTOBUF_VERIFY_VERSION;
  if (argc != 2) {
    cerr << "Usage:  " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
    return -1;
  }
  tutorial::AddressBook address_book;
  {
    // Read the existing address book.
    fstream input(argv[1], ios::in | ios::binary);
    if (!address_book.ParseFromIstream(&input)) {
      cerr << "Failed to parse address book." << endl;
      return -1;
    }
  }
  ListPeople(address_book);
  // Optional:  Delete all global objects allocated by libprotobuf.
  google::protobuf::ShutdownProtobufLibrary();
  return 0;
}

擴展一個Protocol Buffer

當一段時間以後你須要在你發佈使用你的protocol buffer後改進你的protocol buffer定義。若是你但願你的新buffer可以向前兼容,而你的老buffer能向後兼容,那麼你就須要遵照下面這幾個規則:

  • 不要修改tag數字
  • 不要增刪任何required屬性
  • 能夠刪除repeated或者optional屬性
  • 能夠添加 repeated或者optional屬性,可是必須使用新tag number

轉自:https://zybuluo.com/anboqing/note/263467

參考:http://www.cnblogs.com/shitouer/archive/2013/04/08/google-protocol-buffers-overview.html

相關文章
相關標籤/搜索