C++程式員Protocol Buffers基礎指南

來源:互聯網
上載者:User
這篇教程提供了一個面向 C++ 程式員關於 protocol buffers 的基礎介紹。通過建立一個簡單的應用程式範例,它將向我們展示:

在 .proto 檔案中定義訊息格式

使用 protocol buffer 編譯器

使用 C++ protocol buffer API 讀寫訊息

這不是一個關於在 C++ 中使用 protocol buffers 的全面指南。要擷取更詳細的資訊,請參考 Protocol Buffer Language Guide 和 Encoding Reference。

為什麼使用 Protocol Buffers

我們接下來要使用的例子是一個非常簡單的"地址簿"應用程式,它能從檔案中讀取連絡人詳細資料。地址簿中的每一個人都有一個名字、ID、郵件地址和聯絡電話。

如何序列化和擷取結構化的資料?這裡有幾種解決方案:

以二進位形式發送/接收原生的記憶體資料結構。通常,這是一種脆弱的方法,因為接收/讀取代碼必須基於完全相同的記憶體布局、大小端等環境進行編譯。同時,當檔案增加時,原始格式資料會隨著與該格式相關的軟體而迅速擴散,這將導致很難擴充檔案格式。

你可以創造一種 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 格式支援格式的擴充,代碼仍然可以讀取以舊格式編碼的資料。

在哪可以找到範例程式碼

範例程式碼被包含於原始碼包,位於“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)。訊息只是一個包含一系列類型欄位的集合。大多標準的單一資料型別是可以作為欄位類型的,包括 bool、int32、float、double 和 string。你也可以通過使用其他訊息類型作為欄位類型,將更多的資料結構添加到你的訊息中——在以上的樣本,Person 訊息包含了 PhoneNumber 訊息,同時 AddressBook 訊息包含 Person 訊息。你甚至可以定義嵌套在其他訊息內的訊息類型——如你所見,PhoneNumber 類型定義於 Person 內部。如果你想要其中某一個欄位的值是預定義值列表中的某個值,你也可以定義 enum 類型——這兒你可以指定一個電話號碼是 MOBILE、HOME 或 WORK 中的某一個。

每一個元素上的 = 1、= 2 標記確定了用於二進位編碼的唯一“標籤”(tag)。標籤數字 1-15 的編碼比更大的數字少需要一個位元組,因此作為一種最佳化,你可以將這些標籤用於經常使用的元素或 repeated 元素,剩下 16 以及更高的標籤用於非經常使用的元素或 optional 元素。每一個 repeated 欄位的元素需要重新編碼標籤數字,因此 repeated 欄位適合於使用這種最佳化手段。

每一個欄位必須使用下面的修飾符加以標註:

required:必須提供該欄位的值,否則訊息會被認為是 “未初始化的”(uninitialized)。如果 libprotobuf 以偵錯模式編譯,序列化未初始化的訊息將引起一個宣告失敗。以最佳化形式構建,將會跳過檢查,並且無論如何都會寫入該訊息。然而,解析未初始化的訊息總是會失敗(通過 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 可能會遇到問題——舊版本的讀者(LCTT 譯註:即讀取、解析舊版本 Protocol Buffer 訊息的一方)會認為不含該欄位的訊息是不完整的,從而有可能會拒絕解析。在這種情況下,你應該考慮編寫特別針對於應用程式的、自訂的訊息校正函數。Google 的一些工程師得出了一個結論:使用 required 弊多於利;他們更願意使用 optional 和 repeated 而不是 required。當然,這個觀點並不具有普遍性。

編譯你的 Protocol Buffers

既然你有了一個 .proto,那你需要做的下一件事就是產生一個將用於讀寫 AddressBook 訊息的類(從而包括 Person 和 PhoneNumber)。為了做到這樣,你需要在你的 .proto 上運行 protocol buffer 編譯器 protoc:

如果你沒有安裝編譯器,請下載這個包,並按照 README 中的指令進行安裝。

現在運行編譯器,指定來源目錄(你的應用程式原始碼位於哪裡——如果你沒有提供任何值,將使用目前的目錄)、目標目錄(你想要產生的程式碼放在哪裡;常與 $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

讓我們看看產生的一些代碼,瞭解一下編譯器為你建立了什麼類和函數。如果你查看 addressbook.pb.h,你可以看到有一個在 addressbook.proto 中指定所有訊息的類。關注 Person 類,可以看到編譯器為每個欄位產生了讀寫函數(accessors)。例如,對於 name、id、email 和 phone 欄位,有下面這些方法:(LCTT 譯註:此處原文所指檔案名稱有誤,徑該之。)

// 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)集合,而 name 和 email 欄位有兩個額外的方法,因為它們是字串——一個是可以獲得字串直接指標的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。

枚舉和嵌套類

與 .proto 的枚舉相對應,產生的程式碼包含了一個 PhoneType 枚舉。你可以通過 Person::PhoneType 引用這個類型,通過 Person::MOBILE、Person::HOME 和 Person::WORK 引用它的值。(實現細節有點複雜,但是你無須瞭解它們而可以直接使用)

編譯器也產生了一個 Person::PhoneNumber 的嵌套類。如果你查看代碼,你可以發現真正的類型為 Person_PhoneNumber,但它通過在 Person 內部使用 typedef 定義,使你可以把 Person_PhoneNumber 當成嵌套類。唯一產生影響的一個例子是,如果你想要在其他檔案前置聲明該類——在 C++ 中你不能前置聲明嵌套類,但是你可以前置聲明 Person_PhoneNumber。

標準的訊息方法

所有的訊息方法都包含了許多別的方法,用於檢查和操作整個訊息,包括:

bool IsInitialized() const; :檢查是否所有 required 欄位已經被設定。

string DebugString() const; :返回人類可讀的訊息表示,對調試特別有用。

void CopyFrom(const Person& from);:使用給定的值重寫訊息。

void Clear();:清除所有元素為空白的狀態。

上面這些方法以及下一節要講的 I/O 方法實現了被所有 C++ protocol buffer 類共用的訊息(Message)介面。為了擷取更多資訊,請查看 complete API documentation for Message。

解析和序列化

最後,所有 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 檔案)。在那種情況下,你可以用封裝類來設計介面,以更好地適應你的應用程式的特定環境:隱藏一些資料和方法,暴露一些便於使用的函數,等等。但是你絕對不要通過繼承產生的類來添加行為。這樣做的話,會破壞其內部機制,並且不是一個好的物件導向的實踐。

寫訊息

現在我們嘗試使用 protocol buffer 類。你的地址簿程式想要做的第一件事是將個人詳細資訊寫入到地址簿檔案。為了做到這一點,你需要建立、填充 protocol buffer 類執行個體,並且將它們寫入到一個輸出資料流(output stream)。

這裡的程式可以從檔案讀取 AddressBook,根據使用者輸入,將新 Person 添加到 AddressBook,並且再次將新的 AddressBook 寫迴文件。這部分直接調用或引用 protocol buffer 類的代碼會以“// pb”標出。

#include  #include  #include  #include "addressbook.pb.h" // pb 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);   // pb   cin.ignore(256, '\n');   cout << "Enter name: ";   getline(cin, *person->mutable_name());    // pb   cout << "Enter email address (blank for none): ";   string email;   getline(cin, email);   if (!email.empty()) { // pb     person->set_email(email);   // pb   }   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();  //pb     phone_number->set_number(number);   // pb     cout << "Is this a mobile, home, or work phone? ";     string type;     getline(cin, type);     if (type == "mobile") {       phone_number->set_type(tutorial::Person::MOBILE); // pb     } else if (type == "home") {       phone_number->set_type(tutorial::Person::HOME);   // pb     } else if (type == "work") {       phone_number->set_type(tutorial::Person::WORK);   // pb     } 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;   // pb   if (argc != 2) {     cerr << "Usage:  " << argv[0] << " ADDRESS_BOOK_FILE" << endl;     return -1;   }   tutorial::AddressBook address_book;   // pb   {     // 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)) {    // pb       cerr << "Failed to parse address book." << endl;       return -1;     }   }   // Add an address.   PromptForAddress(address_book.add_person());  // pb   {     // Write the new address book back to disk.     fstream output(argv[1], ios::out | ios::trunc | ios::binary);     if (!address_book.SerializeToOstream(&output)) {    // pb       cerr << "Failed to write address book." << endl;       return -1;     }   }   // Optional:  Delete all global objects allocated by libprotobuf.   google::protobuf::ShutdownProtobufLibrary();  // pb   return 0; }

注意 GOOGLE_PROTOBUF_VERIFY_VERSION 宏。它是一種好的實踐——雖然不是嚴格必須的——在使用 C++ Protocol Buffer 庫之前執行該宏。它可以保證避免不小心連結到一個與編譯的標頭檔版本不相容的庫版本。如果被檢查出來版本不匹配,程式將會終止。注意,每個 .pb.cc 檔案在初始化時會自動調用這個宏。

同時注意在程式最後調用 ShutdownProtobufLibrary()。它用於釋放 Protocol Buffer 庫申請的所有全域對象。對大部分程式,這不是必須的,因為雖然程式只是簡單退出,但是 OS 會處理釋放程式的所有記憶體。然而,如果你使用了記憶體流失偵查工具,工具要求全部對象都要釋放,或者你正在寫一個 Protocol Buffer 庫,該庫可能會被一個進程多次載入和卸載,那麼你可能需要強制 Protocol Buffer 清除所有東西。

讀取訊息

當然,如果你無法從它擷取任何資訊,那麼這個地址簿沒多大用處!這個樣本讀取上面例子建立的檔案,並列印檔案裡的所有內容。

#include  #include  #include  #include "addressbook.pb.h" // pb using namespace std; // Iterates though all people in the AddressBook and prints info about them. void ListPeople(const tutorial::AddressBook& address_book) {    // pb   for (int i = 0; i < address_book.person_size(); i++) {        // pb     const tutorial::Person& person = address_book.person(i);    // pb     cout << "Person ID: " << person.id() << endl;   // pb     cout << "  Name: " << person.name() << endl;    // pb     if (person.has_email()) {   // pb       cout << "  E-mail address: " << person.email() << endl;   // pb     }     for (int j = 0; j < person.phone_size(); j++) { // pb       const tutorial::Person::PhoneNumber& phone_number = person.phone(j);  // pb       switch (phone_number.type()) {    // pb         case tutorial::Person::MOBILE:  // pb           cout << "  Mobile phone #: ";           break;         case tutorial::Person::HOME:    // pb           cout << "  Home phone #: ";           break;         case tutorial::Person::WORK:    // pb           cout << "  Work phone #: ";           break;       }       cout << phone_number.number() << endl;    // ob     }   } } // 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;   // pb   if (argc != 2) {     cerr << "Usage:  " << argv[0] << " ADDRESS_BOOK_FILE" << endl;     return -1;   }   tutorial::AddressBook address_book;   // pb   {     // Read the existing address book.     fstream input(argv[1], ios::in | ios::binary);     if (!address_book.ParseFromIstream(&input)) {   // pb       cerr << "Failed to parse address book." << endl;       return -1;     }   }   ListPeople(address_book);   // Optional:  Delete all global objects allocated by libprotobuf.   google::protobuf::ShutdownProtobufLibrary();  // pb   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 欄位,新代碼將無法辨別它被留空(被新代碼)或者從沒被設定(被舊代碼),因為 repeated 欄位沒有 has_ 標誌。

最佳化技巧

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

儘可能複用訊息對象。即使它們被清除掉,訊息也會盡量儲存所有被分配來重用的記憶體。因此,如果我們正在處理許多相同類型或一系列相似結構的訊息,一個好的辦法是重用相同的訊息對象,從而減少記憶體配置的負擔。但是,隨著時間的流逝,對象可能會膨脹變大,尤其是當你的訊息尺寸(LCTT 譯註:各訊息內容不同,有些訊息內容多一些,有些訊息內容少一些)不同的時候,或者你偶爾建立了一個比平常大很多的訊息的時候。你應該自己通過調用 SpaceUsed 方法監測訊息對象的大小,並在它太大的時候刪除它。

對於在多線程中分配大量小對象的情況,你的作業系統記憶體 Clerk可能最佳化得不夠好。你可以嘗試使用 google 的 tcmalloc。

進階用法

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

protocol 訊息類所提供的一個關鍵特性就是反射(reflection)。你不需要針對一個特殊的訊息類型編寫代碼,就可以遍曆一個訊息的欄位並操作它們的值。一個使用反射的有用方法是 protocol 訊息與其他編碼互相轉換,比如 XML 或 JSON。反射的一個更進階的用法可能就是可以找出兩個相同類型的訊息之間的區別,或者開發某種“協議訊息的Regex”,利用Regex,你可以對某種訊息內容進行匹配。只要你發揮你的想像力,就有可能將 Protocol Buffers 應用到一個更廣泛的、你可能一開始就期望解決的問題範圍上。

  • 相關文章

    聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.