Protobuf使用
github地址
目錄
- proto3的更新
- 定義協議格式
- 編譯protobuf
- protobuf_API
- 枚舉和嵌套類
- 標準消息方法
- 解析和序列化
- 寫一條消息
- 閱讀消息
- 編譯
- Protobuf擴展
- 優化
- 高級用法
proto3的更新
- 在第一行非空白非注釋行,必須寫:
syntax = "proto3";
-
字段規則移除了
required
,并把optional
改名為singular
;
在proto2
中required
也是不推薦使用的。proto3
直接從語法層面上移除了required
規則。其實可以做的更徹底,把所有字段規則描述都撤銷,原來的repeated
改為在類型或字段名后加一對中括號。這樣是不是更簡潔? -
repeated
字段默認采用packed
編碼;
在proto2
中,需要明確使用[packed=true]
來為字段指定比較緊湊的packed
編碼方式。 -
移除了
default
選項;
在proto2
中,可以使用default
選項為某一字段指定默認值。在proto3
中,字段的默認值只能根據字段類型由系統決定。也就是說,默認值全部是約定好的,而不再提供指定默認值的語法。
在字段被設置為默認值的時候,該字段不會被序列化。這樣可以節省空間,提高效率。
但這樣就無法區分某字段是根本沒賦值,還是賦值了默認值。這在proto3
中問題不大,但在proto2
中會有問題。
比如,在更新協議的時候使用default
選項為某個字段指定了一個與原來不同的默認值,舊代碼獲取到的該字段的值會與新代碼不一樣。 -
枚舉類型的第一個字段必須為 0 ;
-
移除了對分組的支持;
分組的功能完全可以用消息嵌套的方式來實現,并且更清晰。在proto2
中已經把分組語法標注為『過期』了。這次也算清理垃圾了。 -
移除了對擴展的支持,新增了
Any
類型;
Any
類型是用來替代proto2
中的擴展的。目前還在開發中。
proto2
中的擴展特性很像Swift
語言中的擴展。理解起來有點困難,使用起來更是會帶來不少混亂。
相比之下,proto3
中新增的Any
類型有點像C/C++
中的void*
,好理解,使用起來邏輯也更清晰。 -
增加了
JSON
映射特性;
語言的活力來自于與時俱進。當前,JSON
的流行有其充分的理由。很多『現代化』的語言都內置了對JSON
的支持,比如Go
、PHP
等。而C++
這種看似包羅萬象的學院派語言,因循守舊、故步自封,以致于現出了式微的苗頭。 -
map支持
map<key_type, value_type> map_field = N;
example:
map<string, Project> projects = 3;
- 在 proto3 中,純數字類型的 repeated 字段編碼時候默認采用 packed 編碼(具體原因見 Protocol Buffer 編碼原理 這一章節)
定義協議格式
.proto
文件中的定義很簡單:為要序列化的每個數據結構添加消息,然后為消息中的每個字段指定名稱和類型。這是.proto
定義您的消息的文件addressbook.proto
。
(好的.proto
文件命名風格是:packagename.messagename.proto
)
syntax = "proto3";package tutorial;message Person {string name = 1;int32 id = 2;string email = 3;enum PhoneType {MOBILE = 0;HOME = 1;WORK = 2;}message PhoneNumber {string number = 1;PhoneType type = 2;}repeated PhoneNumber phones = 4;
}message AddressBook {repeated Person people = 1;
}
該.proto
文件以包聲明開頭,這有助于防止不同項目之間的命名沖突。在C++
中,生成的類將放在與包名匹配的命名空間中。
每個元素上的“= 1”,“= 2”標記標識該字段在二進制編碼中使用的唯一“標記”。標簽號1-15需要少于一個字節來編碼而不是更高的數字,因此作為優化,您可以決定將這些標簽用于常用或重復的元素,將標簽16和更高版本留給不太常用的可選元素。重復字段中的每個元素都需要重新編碼標記號,因此重復字段特別適合此優化。
可以指定的最小字段編號為1,最大字段編號為229-1 或 536,870,911。也不能使用數字 19000 到 19999(FieldDescriptor :: kFirstReservedNumber 到 FieldDescriptor :: kLastReservedNumber),因為它們是為 Protocol Buffers實現保留的。
必須使用以下修飾符之一注釋每個字段:
-
required(proto3中移除):必須提供該字段的值,否則該消息將被視為“未初始化”。如果
libprotobuf
在調試模式下編譯,則序列化未初始化的消息將導致斷言失敗。在優化的構建中,將跳過檢查并始終寫入消息。但是,解析未初始化的消息將始終失敗(通過false
從解析方法返回)。除此之外,必填字段的行為與可選字段完全相同。 -
optional(proto3中為singular):該字段可能已設置,也可能未設置。如果未設置可選字段值,則使用默認值。對于簡單類型,您可以指定自己的默認值,就像我們
type
在示例中為電話號碼所做的那樣。否則,使用系統默認值:數字類型為0,字符串為空字符串,bools
為false
。對于嵌入式消息,默認值始終是消息的“默認實例”或“原型”,其中沒有設置其字段。調用訪問器以獲取尚未顯式設置的可選(或必需)字段的值始終返回該字段的默認值。 -
repeated(proto3默認采用 packed 編碼):該字段可以重復任意次數(包括零)。重復值的順序將保留在協議緩沖區中。將重復字段視為動態大小的數組。
-
proto3 中移除了default選項:字段的默認值只能根據字段類型由系統決定。也就是說,默認值全部是約定好的,而不再提供指定默認值的語法。在字段被設置為默認值的時候,該字段不會被序列化。這樣可以節省空間,提高效率。
編譯protobuf
現在運行編譯器,指定源目錄(應用程序的源代碼所在的位置 - 如果不提??供值,則使用當前目錄),目標目錄(您希望生成的代碼在哪里;通常相同$SRC_DIR
) ,以及你的道路.proto
。:
protoc -I = $ SRC_DIR --cpp_out = $ DST_DIR $ SRC_DIR / addressbook.proto
這里都生成到當前目錄,輸入
protoc -I=. --cpp_out=. ./addressbook.proto
protoc --cpp_out=. addressbook.proto // 這種也可以
因為您需要C++
類,所以使用該--cpp_out
選項 - 為其他支持的語言提供了類似的選項。
這將在指定的目標目錄中生成以下文件:
addressbook.pb.h
,標頭聲明您生成的類。addressbook.pb.cc
,其中包含您的類的實現。
protobuf_API
addressbook.pb.h
中,可以看到指定的每條消息都有一個類addressbook.proto
。對于Person
類,可以看到編譯器已為每個字段生成了訪問器。 例如,對于名稱,ID
,電子郵件和電話字段,有以下方法:
// 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();// phones
inline int phones_size() const;
inline void clear_phones();
inline const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phones() const;
inline ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phones();
inline const ::tutorial::Person_PhoneNumber& phones(int index) const;
inline ::tutorial::Person_PhoneNumber* mutable_phones(int index);
inline ::tutorial::Person_PhoneNumber* add_phones();
對于字符串 : 一個mutable_
讓你獲得指向字符串的直接指針的getter
,以及一個額外的setter
。請注意,mutable_email()
即使email
尚未設置,您也可以進行呼叫; 它將自動初始化為空字符串。如果你在這個例子中有一個單數的消息字段,它也有一個mutable_
方法但不是一個set_
方法。
重復的字段也有一些特殊的方法 - 如果你看一下repeated phones
字段的方法,你會發現你可以
-
檢查重復的字段的
_size
(換句話說,有多少電話號碼與此相關聯的Person
). -
使用索引獲取指定的電話號碼.
-
更新指定索引處的現有電話號碼.
-
在郵件中添加另一個電話號碼然后可以編輯(重復的標量類型
add_
只允許您傳入新值).
有關協議編譯器為任何特定字段定義生成的確切成員的詳細信息,請參閱C ++生成的代碼參考。
枚舉和嵌套類
生成的代碼包含PhoneType
與您的.proto
枚舉對應的枚舉。您可以參考這個類型Person::PhoneType
及其作為值的Person::MOBILE
,Person::HOME
和Person::WORK
(實現細節是稍微復雜一點,但你并不需要了解他們使用ENUM
)。
編譯器還為您生成了一個嵌套類Person::PhoneNumber
。如果查看代碼,可以看到實際調用了“真實”類Person_PhoneNumber
,但是在內部定義的typedef Person
允許您將其視為嵌套類。唯一不同的情況是,如果你想在另一個文件中轉發聲明類 - 你不能在C++
中轉發聲明嵌套類型,但你可以轉發聲明Person_PhoneNumber
。
標準消息方法
每個消息類還包含許多其他方法,可用于檢查或操作整個消息,包括:
-
bool IsInitialized() const; 檢查是否已設置所有必填字段。
-
string DebugString() const; 返回消息的人類可讀表示,對調試特別有用。
-
void CopyFrom(const Person& from); 使用給定消息的值覆蓋消息。
-
void Clear(); 清除所有元素回到空狀態。
以下部分中描述的這些和I / O
方法實現了Message
所有C ++
協議緩沖區類共享的接口。有關詳細信息,請參閱完整的API文檔Message。
解析和序列化
最后,每個協議緩沖區類都有使用協議緩沖區二進制格式編寫和讀取所選類型消息的方法。這些包括:
-
bool SerializeToString(string output) const;* 序列化消息并將字節存儲在給定的字符串中。請注意,字節是二進制的,而不是文本; 我們只將該
string
類用作方便的容器。 -
bool ParseFromString(const string& data); 解析給定字符串中的消息。
-
bool SerializeToOstream(ostream output) const;* 將消息寫入給定的
C++ ostream
。 -
bool ParseFromIstream(istream input);* 解析來自給定
C++
的消息istream
。
這些只是解析和序列化提供的幾個選項。再次,請參閱MessageAPI參考以獲取完整列表。
寫一條消息
現在嘗試使用協議緩沖類。地址簿應用程序能夠做的第一件事是將個人詳細信息寫入地址簿文件。為此,需要創建并填充協議緩沖區類的實例,然后將它們寫入輸出流。
這是一個程序,它從文件中讀取一個AddressBook
,根據用戶輸入在AddressBook
文件中添加一個新的Person
,然后再將新文本寫回文件。直接調用或引用協議編譯器生成的代碼的部分將突出顯示。
#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_phones();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_people());{// 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 ++
協議緩沖區庫之前執行此宏是一種很好的做法 - 盡管不是絕對必要的。它驗證您沒有意外鏈接到與您編譯的標頭版本不兼容的庫版本。如果檢測到版本不匹配,程序將中止。請注意,每個.pb.cc
文件在啟動時都會自動調用此宏。
還要注意ShutdownProtobufLibrary()
程序結束時的調用。所有這一切都是刪除協議緩沖區庫分配的所有全局對象。對于大多數程序來說這是不必要的,因為該過程無論如何都要退出,操作系統將負責回收其所有內存。但是,如果您使用需要釋放每個最后一個對象的內存泄漏檢查程序,或者您正在編寫可以由單個進程多次加載和卸載的庫,那么您可能希望強制協議緩沖區清除所有內容。
閱讀消息
當然,如果無法從中獲取任何信息,那么地址簿就不會有多大用處!此示例讀取上面示例創建的文件并打印其中的所有信息。
#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.people_size(); i++) {const tutorial::Person& person = address_book.people(i);cout << "Person ID: " << person.id() << endl;cout << " Name: " << person.name() << endl;if (person.email() != "") {cout << " E-mail address: " << person.email() << endl;}for (int j = 0; j < person.phones_size(); j++) {const tutorial::Person::PhoneNumber& phone_number = person.phones(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;
}
編譯
g++ -Wall -std=c++11 write.cpp addressbook.pb.cc -o write `pkg-config --cflags --libs protobuf`
g++ -Wall -std=c++11 read.cpp addressbook.pb.cc -o read `pkg-config --cflags --libs protobuf`
Protobuf擴展
在釋放使用協議緩沖區的代碼之后遲早,您無疑會想要“改進”協議緩沖區的定義。如果你希望你的新緩沖區向后兼容,并且你的舊緩沖區是向前兼容的 - 而且你幾乎肯定想要這個 - 那么你需要遵循一些規則。在新版本的協議緩沖區中:
-
不得更改任何現有字段的標記號。
-
不得添加或刪除任何必填字段。
-
可以刪除可選或重復的字段。
-
可以添加新的可選或重復字段,但必須使用新的標記號(即從未在此協議緩沖區中使用的標記號,甚至不包括已刪除的字段)。
(這些規則有一些例外,但它們很少使用。)
如果您遵循這些規則,舊代碼將很樂意閱讀新消息并簡單地忽略任何新字段。對于舊代碼,已刪除的可選字段將只具有其默認值,刪除的重復字段將為空。新代碼也將透明地讀取舊消息。但是,請記住舊的消息中不會出現新的可選字段,因此您需要明確檢查它們是否已設置has_
,或者在.proto
文件中提供合理的默認值[default = value]
標簽號后面。如果未為可選元素指定默認值,則使用特定于類型的默認值:對于字符串,默認值為空字符串。對于布爾值,默認值為false
。對于數字類型,默認值為零。另請注意,如果添加了新的重復字段,則新代碼將無法判斷它是否為空(通過新代碼)或從未設置(通過舊代碼),因為沒有has_
標記。
優化
C ++
協議緩沖區庫經過了極大的優化。但是,正確使用可以進一步提高性能。以下是從庫中擠出最后一滴速度的一些提示:
盡可能重用消息對象。消息嘗試保留它們分配用于重用的任何內存,即使它們被清除。因此,如果您連續處理具有相同類型和類似結構的許多消息,則每次重新使用相同的消息對象來加載內存分配器是個好主意。但是,隨著時間的推移,對象會變得臃腫,特別是如果您的消息在“形狀”上有所不同,或者您偶爾構造的消息比平常大得多。
您應該通過調用SpaceUsed方法來監視消息對象的大小,并在它們變得太大時刪除它們。
您的系統內存分配器可能沒有針對從多個線程分配大量小對象進行良好優化。請嘗試使用Google的tcmalloc。
高級用法
協議緩沖區的用途不僅僅是簡單的訪問器和序列化。請務必瀏覽C ++ API參考,以了解您可以使用它們做些什么。
協議消息類提供的一個關鍵特性是反射。您可以迭代消息的字段并操縱它們的值,而無需針對任何特定的消息類型編寫代碼。使用反射的一種非常有用的方法是將協議消息轉換為與其他編碼(例如XML
或JSON
)之間的轉換。更高級的反射使用可能是找到兩個相同類型的消息之間的差異,或者開發一種“協議消息的正則表達式”,您可以在其中編寫與某些消息內容匹配的表達式。如果您運用自己的想象力,可以將協議緩沖區應用于比您最初預期更廣泛的問題!
Message::Reflection界面 提供反射。