[翻譯]Protocol Buffer 基礎: C++

Protocol Buffer Basics: C++

這篇教程提供了一個面向 C++ 程序員、關於 protocol buffers 的基礎介紹。經過建立一個簡單的示例應用程序,它將向咱們展現:ios

  • .proto 文件中定義消息格式
  • 使用 protocol buffer 編譯器
  • 使用 C++ protocol buffer API 讀寫消息

這不是一個關於使用 C++ protocol buffers 的全面指南。要獲取更詳細的信息,請參考 Protocol Buffer Language GuideEncoding Reference程序員

爲何使用 Protocol Buffers

咱們接下來要使用的例子是一個很是簡單的"地址簿"應用程序,它能從文件中讀取聯繫人詳細信息。地址簿中的每個人都有一個名字,ID,郵件地址和聯繫電話。正則表達式

如何序列化和獲取結構化的數據?這裏有幾種解決方案:api

  • 以二進制形式發送/接收原生的內存數據結構。一般,這是一種脆弱的方法,由於接收/讀取代碼的編譯必須基於徹底相同的內存佈局、大小端等等。同時,當文件增長時,原始格式數據會隨着與該格式相連的軟件拷貝而迅速擴散,這將很難擴展文件格式。數組

  • 你能夠創造一種 ad-hoc 方法,將數據項編碼爲一個字符串——好比將 4 個整數編碼爲 "12:3:-23:67"。雖然它須要編寫一次性的編碼和解碼代碼且解碼須要耗費小的運行時成本,但這是一種簡單靈活的方法。這最適合編碼很是簡單的數據。數據結構

  • 序列化數據爲 XML。這種方法是很是吸引人的,由於 XML 是一種適合人閱讀的格式,而且有爲許多語言開發的庫。若是你想與其餘程序和項目共享數據,這多是一種不錯的選擇。然而,衆所周知,XML 是空間密集型的,且在編碼和解碼時,它對程序會形成巨大的性能損失。同時,使用 XML DOM 樹被認爲比操做一個類的簡單字段更加複雜。多線程

Protocol buffers 是針對這個問題的一種靈活、高效、自動化的解決方案。使用 Protocol buffers,你須要寫一個 .proto 說明,用於描述你所但願存儲的數據結構。利用 .proto 文件,protocol buffer 編譯器能夠建立一個類,用於實現自動化編碼和解碼高效的二進制格式的 protocol buffer 數據。產生的類提供了構造 protocol buffer 的字段的 getters 和 setters,而且做爲一個單元,關注讀寫 protocol buffer 的細節。重要的是,protocol buffer 格式支持擴展格式,代碼仍然能夠讀取以舊格式編碼的數據。ide

在哪能夠找到示例代碼

示例代碼被包含於源代碼包,位於 "examples" 文件夾。在下載代碼。函數

定義你的協議格式

爲了建立本身的地址簿應用程序,你須要從 .proto 開始。.proto 文件中的定義很簡單:爲你所須要序列化的數據結構添加一個消息(message),而後爲消息中的每個字段指定一個名字和類型。這裏是定義你消息的 .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;
}

如你所見,其語法相似於 C++ 或 Java。咱們開始看看文件的每一部份內容作了什麼。

.proto 文件以一個 package 聲明開始,這能夠避免不一樣項目的命名衝突。在 C++,你生成的類會被置於與 package 名字同樣的命名空間。

下一步,你須要定義消息(message)。消息只是一個包含一系列類型字段的集合。大多標準簡單數據類型是能夠做爲字段類型的,包括 boolint32floatdoublestring。你也能夠經過使用其餘消息類型做爲字段類型,將更多的數據結構添加到你的消息中——在以上的示例,Person 消息包含了 PhoneNumber 消息,同時 AddressBook 消息包含 Person 消息。你甚至能夠定義嵌套在其餘消息內的消息類型——如你所見,PhoneNumber 類型定義於 Person 內部。若是你想要其中某一個字段擁有預約義值列表中的某個值,你也能夠定義 enum 類型——這兒你想指定一個電話號碼能夠是 MOBILEHOMEWORK 中的某一個。

每個元素上的 「=1」、"=2" 標記肯定了用於二進制編碼的惟一"標籤"(tag)。標籤數字 1-15 的編碼比更大的數字少須要一個字節,所以做爲一種優化,你能夠將這些標籤用於常用或 repeated 元素,剩下 16 以及更高的標籤用於非常用或 optional 元素。每個 repeated 字段的元素須要從新編碼標籤數字,所以 repeated 字段對於這優化是一個特別好的候選者。

每個字段必須使用下面的修飾符加以標註:

  • required:必須提供字段的值,不然消息會被認爲是 "未初始化的"(uninitialized)。若是 libprotobuf 以 debug 模式編譯,序列化未初始化的消息將引發一個斷言失敗。以優化形式構建,將會跳過檢查,而且不管如何都會寫入消息。然而,解析未初始化的消息老是會失敗(經過 parse 方法返回 false)。除此以外,一個 required 字段的表現與 optional 字段徹底同樣。

  • optional:字段可能會被設置,也可能不會。若是一個 optional 字段沒被設置,它將使用默認值。對於簡單類型,你能夠指定你本身的默認值,正如例子中咱們對電話號碼的 type 同樣,不然使用系統默認值:數字類型爲 0、字符串爲空字符串、布爾值爲 false。對於嵌套消息,默認值總爲消息的"默認實例"或"原型",它的全部字段都沒被設置。調用 accessor 來獲取一個沒有顯式設置的 optional(或 required) 字段的值老是返回字段的默認值。

  • repeated:字段能夠重複任意次數(包括 0)。repeated 值的順序會被保存於 protocol buffer。能夠將 repeated 字段想象爲動態大小的數組。

你能夠查找關於編寫 .proto 文件的完整指導——包括全部可能的字段類型——在 Protocol Buffer Language Guide。不要在這裏面查找與類繼承類似的特性,由於 protocol buffers 不會作這些。

required 是永久性的,在把一個字段標識爲 required 的時候,你應該特別當心。若是在某些狀況下你不想寫入或者發送一個 required 的字段,那麼將該字段更改成 optional 可能會遇到問題——舊版本的讀者(譯者注:即讀取、解析舊版本 Protocol Buffer 消息的一方)會認爲不含該字段的消息是不完整的,從而有可能會拒絕解析。在這種狀況下,你應該考慮編寫特別針對於應用程序的、自定義的消息校驗函數。Google 的一些工程師得出了一個結論:使用 required 弊多於利;他們更願意使用 optional 和 repeated 而不是 required。固然,這個觀點並不具備廣泛性。

編譯你的 Protocol Buffers

既然你有了一個 .proto,那你須要作的下一件事就是生成一個將用於讀寫 AddressBook 消息的類(從而包括 PersonPhoneNumber)。爲了作到這樣,你須要在你的 .proto 上運行 protocol buffer 編譯器 protoc

  1. 若是你沒有安裝編譯器,請下載這個包,並按照 README 中的指令進行安裝。
  2. 如今運行編譯器,知道源目錄(你的應用程序源代碼位於哪裏——若是你沒有提供任何值,將使用當前目錄),目標目錄(你想要生成的代碼放在哪裏;常與 $SRC_DIR 相同),而且你的 .proto 路徑。在此示例,你...:
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto

由於你想要 C++ 的類,因此你使用了 --cpp_out 選項——也爲其餘支持的語言提供了相似選項。

在你指定的目標文件夾,將生成如下的文件:

  • addressbook.pb.h,聲明你生成類的頭文件。
  • addressbook.pb.cc,包含你的類的實現。

Protocol Buffer API

讓咱們看看生成的一些代碼,瞭解一下編譯器爲你建立了什麼類和函數。若是你查看 tutorial.pb.h,你能夠看到有一個在 tutorial.proto 中指定全部消息的類。關注 Person 類,能夠看到編譯器爲每一個字段生成了讀寫函數(accessors)。例如,對於 nameidemailphone 字段,有下面這些方法:

// 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 的名字與字段的小寫名字徹底同樣,而且 setter 方法以 set_ 開頭。同時每一個單一(singular)(required 或 optional)字段都有 has_ 方法,該方法在字段被設置了值的狀況下返回 true。最後,全部字段都有一個 clear_ 方法,用以清除字段到空(empty)狀態。

數字 id 字段僅有上述的基本讀寫函數集合(accessors),而 nameemail 字段有兩個額外的方法,由於它們是字符串——一個是能夠得到字符串直接指針的mutable_ getter ,另外一個爲額外的 setter。注意,儘管 email 還沒被設置(set),你也能夠調用 mutable_email;由於 email 會被自動地初始化爲空字符串。在本例中,若是你有一個單一的(required 或 optional)消息字段,它會有一個 mutable_ 方法,而沒有 set_ 方法。

repeated 字段也有一些特殊的方法——若是你看看 repeated phone 字段的方法,你能夠看到:

  • 檢查 repeated 字段的 _size(也就是說,與 Person 相關的電話號碼的個數)
  • 使用下標取得特定的電話號碼
  • 更新特定下標的電話號碼
  • 添加新的電話號碼到消息中,以後你即可以編輯。(repeated 標量類型有一個 add_ 方法,用於傳入新的值)

爲了獲取 protocol 編譯器爲全部字段定義生成的方法的信息,能夠查看 C++ generated code reference

枚舉和嵌套類(Enums and Nested Classes)

.proto 的枚舉相對應,生成的代碼包含了一個 PhoneType 枚舉。你能夠經過 Person::PhoneType 引用這個類型,經過 Person::MOBILEPerson::HOMEPerson::WORK 引用它的值。(實現細節有點複雜,可是你無須瞭解它們而能夠直接使用)

編譯器也生成了一個 Person::PhoneNumber 的嵌套類。若是你查看代碼,你能夠發現真正的類型爲 Person_PhoneNumber,但它經過在 Person 內部使用 typedef 定義,使你能夠把 Person_PhoneNumber 當成嵌套類。惟一產生影響的一個例子是,若是你想要在其餘文件前置聲明該類——在 C++ 中你不能前置聲明嵌套類,可是你能夠前置聲明 Person_PhoneNumber

標準的消息方法

全部的消息方法都包含了許多別的方法,用於檢查和操做整個消息,包括:

  • bool IsInitialized() const; :檢查是否全部 required 字段已經被設置。
  • string DebugString() const;:返回人類可讀的消息表示,對 debug 特別有用。
  • void CopyFrom(const Person& from);:使用給定的值重寫消息。
  • void Clear();:清除全部元素爲空(empty)的狀態。

上面這些方法以及下一節要講的 I/O 方法實現了被全部 C++ protocol buffer 類共享的消息(Message)接口。爲了獲取更多信息,請查看 complete API documentation for Message

解析和序列化(Parsing and Serialization)

最後,全部 protocol buffer 類都有讀寫你選定類型消息的方法,這些方法使用了特定的 protocol buffer 二進制格式。這些方法包括:

  • bool SerializeToString(string* output) const;:序列化消息以及將消息字節數據存儲在給定的字符串。注意,字節數據是二進制格式的,而不是文本格式;咱們只使用 string 類做爲合適的容器。
  • bool ParseFromString(const string& data);:從給定的字符創解析消息。
  • bool SerializeToOstream(ostream* output) const;:將消息寫到給定的 C++ ostream
  • bool ParseFromIstream(istream* input);:從給定的 C++ istream 解析消息。

這些只是兩個用於解析和序列化的選擇。再次說明,能夠查看 Message API reference 完整的列表。

Protocol Buffers 和 面向對象設計的 Protocol buffer 類一般只是純粹的數據存儲器(像 C++ 中的結構體);它們在對象模型中並非一等公民。若是你想向生成的 protocol buffer 類中添加更豐富的行爲,最好的方法就是在應用程序中對它進行封裝。若是你無權控制 .proto 文件的設計的話,封裝 protocol buffers 也是一個好主意(例如,你從另外一個項目中重用一個 .proto 文件)。在那種狀況下,你能夠用封裝類來設計接口,以更好地適應你的應用程序的特定環境:隱藏一些數據和方法,暴露一些便於使用的函數,等等。可是你絕對不要經過繼承生成的類來添加行爲。這樣作的話,會破壞其內部機制,而且不是一個好的面向對象的實踐。

寫消息(Writing A Message)

如今咱們嘗試使用 protocol buffer 類。你的地址簿程序想要作的第一件事是將我的詳細信息寫入到地址簿文件。爲了作到這一點,你須要建立、填充 protocol buffer 類實例,而且將它們寫入到一個輸出流(output stream)。

這裏的程序能夠從文件讀取 AddressBook,根據用戶輸入,將新 Person 添加到 AddressBook,而且再次將新的 AddressBook 寫回文件。這部分直接調用或引用 protocol buffer 類的代碼會高亮顯示。

#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 庫以前執行該宏。它能夠保證避免不當心連接到一個與編譯的頭文件版本不兼容的庫版本。若是被檢查出來版本不匹配,程序將會終止。注意,每一個 .pb.cc 文件在初始化時會自動調用這個宏。

同時注意在程序最後調用 ShutdownProtobufLibrary()。它用於釋放 Protocol Buffer 庫申請的全部全局對象。對大部分程序,這不是必須的,由於雖然程序只是簡單退出,可是 OS 會處理釋放程序的全部內存。然而,若是你使用了內存泄漏檢測工具,工具要求所有對象都要釋放,或者你正在寫一個庫,該庫可能會被一個進程屢次加載和卸載,那麼你可能須要強制 Protocol Buffer 清除全部東西。

讀取消息

固然,若是你沒法從它獲取任何信息,那麼這個地址簿沒多大用處!這個示例讀取上面例子建立的文件,並打印文件裏的全部內容。

#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 的定義。若是你想要新的 buffers 向後兼容,而且老的 buffers 向前兼容——幾乎能夠確定你很渴望這個——這裏有一些規則,你須要遵照。在新的 protocol buffer 版本:

  • 你毫不能夠修改任何已存在字段的標籤數字
  • 你毫不能夠添加或刪除任何 required 字段
  • 你能夠刪除 optional 或 repeated 字段
  • 你能夠添加新的 optional 或 repeated 字段,可是你必須使用新的標籤數字(也就是說,標籤數字在 protocol buffer 中從未使用過,甚至不能是已刪除字段的標籤數字)。

(這是對於上面規則的一些異常狀況,但它們不多用到。)

若是你能遵照這些規則,舊代碼則能夠歡快地讀取新的消息,而且簡單地忽略全部新的字段。對於舊代碼來講,被刪除的 optional 字段將會簡單地賦予默認值,被刪除的 repeated 字段會爲空。新代碼顯然能夠讀取舊消息。然而,請記住新的 optional 字段不會呈如今舊消息中,所以你須要顯式地使用 has_ 檢查它們是否被設置或者在 .proto 文件在標籤數字後使用 [default = value] 提供一個合理的默認值。若是一個 optional 元素沒有指定默認值,它將會使用類型特定的默認值:對於字符串,默認值爲空字符串;對於布爾值,默認值爲 false;對於數字類型,默認類型爲 0。注意,若是你添加一個新的 repeated 字段,新代碼將沒法辨別它被留空(left empty)(被新代碼)或者從沒被設置(被舊代碼),由於 repeated 字段沒有 has_ 標誌。

優化技巧

C++ Protocol Buffer 庫已極度優化過了。可是,恰當的用法可以更多地提升性能。這裏是一些技巧,能夠幫你從庫中擠壓出最後一點速度:

  • 儘量複用消息對象。即便它們被清除掉,消息也會盡可能保存全部被分配來重用的內存。所以,若是咱們正在處理許多相同類型或一系列類似結構的消息,一個好的辦法是重用相同的消息對象,從而減小內存分配的負擔。可是,隨着時間的流逝,對象可能會膨脹變大,尤爲是當你的消息尺寸(譯者注:各消息內容不一樣,有些消息內容多一些,有些消息內容少一些)不一樣的時候,或者你偶爾建立了一個比日常大不少的消息的時候。你應該本身經過調用 SpaceUsed 方法監測消息對象的大小,並在它太大的時候刪除它。

  • 對於在多線程中分配大量小對象的狀況,你的操做系統內存分配器可能優化得不夠好。你能夠嘗試使用 google 的 tcmalloc

高級用法

Protocol Buffers 毫不僅用於簡單的數據存取以及序列化。請閱讀 C++ API reference 來看看你還能用它來作什麼。

protocol 消息類所提供的一個關鍵特性就是反射。你不須要編寫針對一個特殊的消息類型的代碼,就能夠遍歷一個消息的字段並操做它們的值。一個使用反射的有用方法是 protocol 消息與其餘編碼互相轉換,好比 XML 或 JSON。反射的一個更高級的用法可能就是能夠找出兩個相同類型的消息之間的區別,或者開發某種 "協議消息的正則表達式",利用正則表達式,你能夠對某種消息內容進行匹配。只要你發揮你的想像力,就有可能將 Protocol Buffers 應用到一個更普遍的、你可能一開始就指望解決的問題範圍上。

反射是由 Message::Reflection interface 提供的。


via: https://developers.google.com/protocol-buffers/docs/cpptutorial

相關文章
相關標籤/搜索