C++_程序員_Protocol_Buffers_基礎指南
摘要: 這篇教程提供了一個面向 C++ 程序員關於 protocol buffers 的基礎介紹。通過創建一個簡單的示例應用程序,它將向我們展示: 在 .proto 文件中定義消息格式 使用 protocol buffer 編譯器 使用 C++ protocol buffer API 讀寫消息 這不是一個關於在 C++ 中使用 protocol buffers 的全面指南。
這篇教程提供了一個面向 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。
原文鏈接
C++_程序員_Protocol_Buffers_基礎指南