C/C++ProtoBuf使用小結(jié)
一.什么是ProtoBuf
1.序列化和反序列化概念
序列化:把對(duì)象轉(zhuǎn)變?yōu)樽止?jié)序列的過程,稱為系列化。
反序列化:把字節(jié)序列的內(nèi)容恢復(fù)為對(duì)象的過程,稱為反序列化。
2.什么情況下需要序列化和反序列化
存儲(chǔ)數(shù)據(jù):將內(nèi)存中的對(duì)象狀態(tài)保存在文件中或存儲(chǔ)在數(shù)據(jù)庫(kù)中時(shí)。
網(wǎng)絡(luò)傳輸:網(wǎng)絡(luò)傳輸數(shù)據(jù)時(shí),無(wú)法直接傳輸對(duì)象,需要在傳輸前序列化,傳輸完成后反 序列化成對(duì)象,就像學(xué)習(xí)Socket編程中發(fā)送與接收時(shí)。
3.如何實(shí)現(xiàn)序列化
xml,json,protoBuf這三種工具都可以實(shí)現(xiàn)序列化和反序列化
4.ProtoBuf是什么
ProtoBuf是讓數(shù)據(jù)結(jié)構(gòu)序列化的方法,具有以下特點(diǎn):
- 語(yǔ)言無(wú)關(guān)、平臺(tái)無(wú)關(guān):即 ProtoBuf 支持 Java、C++、Python 等多種語(yǔ)言,支持多個(gè)平臺(tái)。
- 高效:即比 XML 更小、更快、更為簡(jiǎn)單。
- 擴(kuò)展性、兼容性好:你可以更新數(shù)據(jù)結(jié)構(gòu),而不影響和破壞原有的舊程序。
二.ProtoBuf簡(jiǎn)單語(yǔ)法知識(shí)
1.文件規(guī)范
創(chuàng)建一個(gè)ProtoBuf文件,后綴一定以.proto結(jié)尾,文件命名全小寫字母,多個(gè)字母之間以_為分隔符,添加注釋的方法和C/C++的一毛一樣。
2.ProtoBuf語(yǔ)法分類
ProtoBuf有對(duì)個(gè)版本,在這里我們使用最新的版本,protobuf3的語(yǔ)法,簡(jiǎn)稱proto3,它是最新的ProtoBuf語(yǔ)法版本。proto3 簡(jiǎn)化了 ProtoBuf 語(yǔ)言,既易于使用,又可以在更廣泛的編程語(yǔ)言中使用。它允許你使用 Java,C++,Python等多種語(yǔ)言生成 protocol buffer 代碼。
3.ProtoBuf3語(yǔ)法
syntax
我們?cè)趧?chuàng)建一個(gè)文件時(shí),一定要在文件開頭致命你所使用的語(yǔ)法,如果沒有指定,默認(rèn)使用Protobuf2的語(yǔ)法來使用,
比如:
syntax = "proto3";
syntax為指定語(yǔ)法,一定要有,就像函數(shù)中一定要指明函數(shù)返回類型一樣。
package(聲明符)
package 是一個(gè)可選的聲明符,能表示 .proto 文件的命名空間,在項(xiàng)目中要有唯一性。它的作用是為了避免我們定義的消息出現(xiàn)沖突,就像c++中的namespace一樣,指定一個(gè)域,防止沖突。
比如:
package person;
message(定義消息)
消息(message):要定義的結(jié)構(gòu)化對(duì)象,可以給這個(gè)結(jié)構(gòu)化對(duì)象中定義對(duì)用的屬性內(nèi)容,就像C//C++中的結(jié)構(gòu)體一樣,只能夠定義對(duì)象。
這里強(qiáng)調(diào)下為什么要定義對(duì)象:
網(wǎng)絡(luò)傳輸中,需要傳輸雙方定制協(xié)議,定制協(xié)議就是定制結(jié)構(gòu)體或者結(jié)構(gòu)化數(shù)據(jù),比如:Tcp,Udp。而且將數(shù)據(jù)存儲(chǔ)在數(shù)據(jù)庫(kù)時(shí),需將數(shù)據(jù)統(tǒng)一為對(duì)象組織起來,在進(jìn)行存儲(chǔ)。
所以ProtoBuf就是以message的方式支持我們定義協(xié)議字段的。比如:
syntax="proto3"; package=package; message people { ; }
定義字段消息
在 message 中我們可以定義其屬性字段,字段定義格式為:字段類型 字段名 = 字段唯一編號(hào),比如:
syntax="proto3"; package=package; message people { int32 age=1; }
int32為字段類型 age為字段名 1為字段唯一編號(hào)
下面是protobuf創(chuàng)建類型及所對(duì)應(yīng)的C/C++類型:
注:[1] 變長(zhǎng)編碼是指:經(jīng)過protobuf 編碼后,原本4字節(jié)或8字節(jié)的數(shù)可能會(huì)被變?yōu)槠渌止?jié)數(shù)。
比如:
syntax="proto3" package sss message person { string name=1; int32 age=2; string sex=3; }
注意:字段唯一編號(hào)范圍:
2^0~2^29-1,其中,19000~19999不可用,在 Protobuf 協(xié)議的實(shí)現(xiàn)中,對(duì)這些數(shù)進(jìn)行了預(yù)留。如果非要在.proto文件中使用這些預(yù)留標(biāo)識(shí)號(hào),例如將 name 字段的編號(hào)設(shè)置為19000,編譯時(shí)就會(huì)報(bào)警。值得一提的是,范圍為 1 ~ 15 的字段編號(hào)需要一個(gè)字節(jié)進(jìn)行編碼, 16 ~ 2047 內(nèi)的數(shù)字需要兩個(gè)字節(jié)進(jìn)行編碼。編碼后的字節(jié)不僅只包含了編號(hào),還包含了字段類型。所以 1 ~ 15 要用來標(biāo)記出現(xiàn)非常頻繁的字段,要為將來有可能添加的、頻繁出現(xiàn)的字段預(yù)留一些出來。
通過上面的學(xué)習(xí),我們來實(shí)現(xiàn)一個(gè)簡(jiǎn)單的test.proto文件,
syntax="proto3"; package sss; message person { string name=1; int32 age=2; string sex=3; }
編輯protobuf命令如下:proto --cpp_out. [文件名]
我們使用的是C++語(yǔ)言,所以編譯之后生成兩個(gè)文件:test.pb.h和test.pb.cc。
對(duì)于編譯生成的 C++ 代碼,包含了以下內(nèi)容 :
- 對(duì)于每個(gè) message ,都會(huì)生成一個(gè)對(duì)應(yīng)的消息類。
- 在消息類中,編譯器為每個(gè)字段提供了獲取和設(shè)置方法,以及一下其他能夠操作字段的方法。
- 編輯器會(huì)針對(duì)于每個(gè) .proto 文件生成.h 和 .cc 文件,分別用來存放類的聲明與類的實(shí)現(xiàn)。
代碼過長(zhǎng),就不展示了,感興趣的自己做下?。。?/p>
編譯命令行格式
protoc [--proto_path=IMPORT_PATH] --cpp_out=DST_DIR path/to/file.proto
protoc:是protobuf提供的命令行編輯工具。
--proto_path: 指定被編輯的所在目錄,可多次指定,可簡(jiǎn)寫為-I。如不指定,則在當(dāng)前目錄下進(jìn)行搜索。
--cpp_out=: 指定編譯后的文件為c++文件。
DST_DIR; 文件路徑
path/to/file.proto:要編譯的文件。
上述的例子中,每個(gè)字段都有設(shè)置和獲取的方法,getter 的名稱與小寫字段完全相同,setter 方法以 set_ 開頭,每個(gè)字段都有一個(gè)clear_的方法,可以將字段設(shè)置為empty狀態(tài)。比如:
#include <iostream> #include "test.ph.h" using namespace sss; using namespace std; int main() { person p1; p1.set_name("張三"); p1.set_age(11); p1.set_sex("nan"); return 0; }
在消息類的父類MessageLift中,提供了讀寫消息實(shí)例的方法,包括序列化方法和反序列化方法。
class MessageLite { public: //序列化: bool SerializeToOstream(ostream* output) const; // 將序列化后數(shù)據(jù)寫入文件 //流 bool SerializeToArray(void *data, int size) const; bool SerializeToString(string* output) const; //反序列化: bool ParseFromIstream(istream* input); // 從流中讀取數(shù)據(jù),再進(jìn)行反序列化 //動(dòng)作 bool ParseFromArray(const void* data, int size); bool ParseFromString(const string& data); };
注意:
序列化的方法為二進(jìn)制方法,非文本格式;以上三種序列化的方法沒有本質(zhì)區(qū)別,只是序列化之后輸出的格式不同,可以提供不同場(chǎng)景使用,序列化的 API 函數(shù)均為const成員函數(shù),因?yàn)樾蛄谢粫?huì)改變類對(duì)象的內(nèi)容, 而是將序列化的結(jié)果保存到函數(shù)入?yún)⒅付ǖ牡刂分小?/p>
更多詳細(xì)API函數(shù)參考:message.h | Protocol Buffers Documentation (protobuf.dev)
序列化和反序列化的使用:看結(jié)果
#include "contact.pb.h" using namespace std; int main() { string people_str; contact::PersonInit p1; p1.set_age(100); p1.set_name("張三"); p1.set_sex("boy"); if (!p1.SerializeToString(&people_str)) { cout << "序列化聯(lián)系人失敗." << endl; } // 打印序列化結(jié)果 cout << "序列化后的 people_str: " << people_str << endl; if(!p1.SerializePartialToString(&people_str)) { cout<<"反序列化失敗:"<<endl; } cout<<"反序列化成功:"<<endl<<people_str<<endl; return 0; }
這就是簡(jiǎn)單的Protobuf的應(yīng)用。
接下來是ProtoBuf的語(yǔ)法詳解。
三.ProtoBuf語(yǔ)法詳解
在這部分,我們使用一個(gè)簡(jiǎn)單的項(xiàng)目推進(jìn)的方式來講解語(yǔ)法內(nèi)容,通過一個(gè)通訊錄的實(shí)現(xiàn),完成我們從最基礎(chǔ)的內(nèi)容到網(wǎng)絡(luò)版本的知識(shí)。
1.字段規(guī)則
singular :消息中可以包含該字段零次或一次(不超過一次)。 proto3 語(yǔ)法中,字段默認(rèn)使用該規(guī)則,可以不用設(shè)置。
repeated :消息中可以包含該字段任意多次(包括零次),其中重復(fù)值的順序會(huì)被保留。可以理解為定義了一個(gè)數(shù)組。
比如:
syntax="proto3"; package contacts2; message person { string name=1; int32 age=2; repeated string phone_num=3; }
2.消息類型的定義與使用
在單個(gè).proto文件中,可以定義多個(gè)message,也可以嵌套定義,每個(gè)message中的字段編號(hào)可以重復(fù),并且,消息字段也可以作為字段類型來使用,比如:
syntax="proto3"; package contacts2; // message Phone{ // string phone_num=1; // } message person { string name=1; int32 age=2; //第一種,單獨(dú)寫法 //repeated Phone=3; //第二種,嵌套寫法 message Phone{ string phone_num=1; } repeated Phone=3; }
也可以將其他.proto文件中定義的消息導(dǎo)入并使用,但一定要加import 比如:
syntax="proto3"; package contacts2; import "Photo.proto";//將其他文件中的消息字段導(dǎo)入 message person { string name=1; int32 age=2; //第三種 repeated Phone.Phone phone=3; } syntax="proto3"; package Phone; message Phone{ string phone_num=1; }
注:在 proto3 文件中可以導(dǎo)入 proto2 消息類型并使用它們,反之亦然
編譯
我們可以將以上的內(nèi)容稍加修改,變?yōu)槲覀兊牡谝话嫱ㄓ嶄洠?/p>
syntax="proto3"; package contacts2; //import "Photo.proto";//將其他文件中的消息字段導(dǎo)入 message peopleInfo { string name=1; int32 age=2; message Phone{ string phone_num=1; } repeated Phone=3; } message Contact{ repeated PeopleInfo contact=1; }
然后編譯,編譯后生成的 contacts.pb.h contacts.pb.cc 會(huì)將老版本的代碼覆蓋掉,由于代碼過長(zhǎng),就不展示啦?。。?/p>
上述的代碼中:
- 每個(gè)字段都有一個(gè) clear_ 方法,可以將字段重新設(shè)置回 empty 狀態(tài)。
- 每個(gè)字段都有設(shè)置和獲取的方法, 獲取方法的方法名稱與小寫字段名稱完全相同。但如果是消息類型的字段,其設(shè)置方法為 mutable_ 方法,返回值為消息類型的指針,這類方法會(huì)為我們開辟好空間,可以直接對(duì)這塊空間的內(nèi)容進(jìn)行修改。
- 對(duì)于使用 repeated 修飾的字段,也就是數(shù)組類型,pb 為我們提供了 add_ 方法來新增一個(gè)值,并且提供了 _size 方法來判斷數(shù)組存放元素的個(gè)數(shù)。
我們可以對(duì)編譯好的代碼試用一下:
#include <iostream> #include <fstream> #include "contact.pb.h" using namespace std; using namespace contacts; void AddPeopleInfo(PeopleInfo *people_info_ptr) { cout << "-------------新增聯(lián)系人-------------" << endl; cout << "請(qǐng)輸入聯(lián)系人姓名: "; string name; getline(cin, name); people_info_ptr->set_name(name);//輸入用set_函數(shù) cout << "請(qǐng)輸入聯(lián)系人年齡: "; int age; cin >> age; people_info_ptr->set_age(age); cin.ignore(256, '\n'); for (int i = 1;; i++) { cout << "請(qǐng)輸入聯(lián)系人電話" << i << "(只輸入回車完成電話新增): "; string number; getline(cin, number); if (number.empty()) { break; } //number需要添加才可以用 PeopleInfo_Phone *phone = people_info_ptr->add_phone(); phone->set_number(number); } cout << "-----------添加聯(lián)系人成功-----------" << endl; } int main() { Contacts contacts; // 先讀取已存在的 contacts fstream input("text.bin",ios::in | ios::binary); if (!input) { cout << ": File not found. Creating a new file." << endl; } else if (!contacts.ParseFromIstream(&input))//序列化 { cerr << "Failed to parse contacts." << endl; input.close(); return -1; } // 新增一個(gè)聯(lián)系人 AddPeopleInfo(contacts.add_contacts()); //向磁盤文件寫入新的 contacts fstream output("test.bin",ios::out | ios::trunc | ios::binary); if (!contacts.SerializeToOstream(&output)) { cerr << "Failed to write contacts." << endl; input.close(); output.close(); return -1; } input.close(); //output.close(); return 0; }
3:enum類型的使用
語(yǔ)法支持我們定義枚舉類型來使用,我們可以定義一個(gè)enum類型的使用,
syntax="proto3"; package Phone; message{ enum PhoneType{ MP=0;//移動(dòng)電話 TEL=1;//固定電話 } }
要注意枚舉類型的定義有以下幾種規(guī)則:
1. 0 值常量必須存在,且要作為第一個(gè)元素。這是為了與 proto2 的語(yǔ)義兼容:第一個(gè)元素作為默認(rèn)值,且值為 0。
2. 枚舉類型可以在消息外定義,也可以在消息體內(nèi)定義(嵌套)。
3. 枚舉的常量值在 32 位整數(shù)的范圍內(nèi)。但因負(fù)值無(wú)效因而不建議使用(與編碼規(guī)則有關(guān))。定義時(shí)注意:
將兩個(gè) ‘具有相同枚舉值名稱’ 的枚舉類型放在單個(gè) .proto 文件下測(cè)試時(shí),編譯后會(huì)報(bào)錯(cuò):某某某常量已經(jīng)被定義!所以這里要注意:
• 同級(jí)(同層)的枚舉類型,各個(gè)枚舉類型中的常量不能重名。
• 單個(gè) .proto 文件下,最外層枚舉類型和嵌套枚舉類型,不算同級(jí)。
• 多個(gè) .proto 文件下,若一個(gè)文件引入了其他文件,且每個(gè)文件都未聲明 package,每個(gè) proto 文件中的枚舉類型都在最外層,算同級(jí)。
• 多個(gè) .proto 文件下,若一個(gè)文件引入了其他文件,且每個(gè)文件都聲明了 package,不算同級(jí)。
下面我們來使用一下:
message Address { string home_address=1; string unit_address=2; } message PeopleInfo { string name=1; int32 age=2; message Phone { string number=1; enum PhoneType { MP = 0; // 移動(dòng)電話 TEL = 1; // 固定電話 } PhoneType type=2; } //多組電話 repeated Phone phone=3; }
編譯生成后的文件:對(duì)于在.proto文件中定義的枚舉類型,編譯生成的代碼中會(huì)含有與之對(duì)應(yīng)的枚舉類型、校驗(yàn)枚舉值是否有效的方法 _IsValid、以及獲取枚舉值名稱的方法 _Name。對(duì)于使用了枚舉類型的字段,包含設(shè)置和獲取字段的方法,已經(jīng)清空字段的方法clear_;
4.any類型
字段還可以聲明為 Any 類型,可以理解為泛型類型。使用時(shí)可以在 Any 中存儲(chǔ)任意消息類型。Any 類型的字段也用 repeated 來修飾,Any 類型是 google 已經(jīng)幫我們定義好的類型,在安裝 ProtoBuf 時(shí),其中的 include 目錄下查找所有g(shù)oogle 已經(jīng)定義好的 .proto 文件,在此目錄下查找:/usr/local/protobuf/include/google/protobuf/
我們可以來使用一下:
message Address { string home_address=1; string unit_address=2; } message PeopleInfo { string name=1; int32 age=2; message Phone { string number=1; enum PhoneType { MP = 0; // 移動(dòng)電話 TEL = 1; // 固定電話 } PhoneType type=2; } //多組電話 repeated Phone phone=3; google.protobuf.Any data = 4; oneof other_contact { // repeated string qq = 5; // 不能使用 repeated string qq = 5; string wechat = 6; } //備注信息 map<string,string> remake=7; }
上述的代碼中,對(duì)于 Any 類型字段:
設(shè)置和獲?。韩@取方法的方法名稱與小寫字段名稱完全相同。設(shè)置方法可以使用 mutable_ 方
法,返回值為Any類型的指針,這類方法會(huì)為我們開辟好空間,可以直接對(duì)這塊空間的內(nèi)容進(jìn)行
修改;之前說過,我們可以在 Any 字段中存儲(chǔ)任意消息類型,這就要涉及到任意消息類型 和 Any 類型的互轉(zhuǎn)。這部分代碼就在 Google為我們寫好的頭文件 any.pb.h 中。使用方法:
- 使用 PackFrom() 方法可以將任意消息類型轉(zhuǎn)為 Any 類型。
- 使用 UnpackTo() 方法可以將 Any 類型轉(zhuǎn)回之前設(shè)置的任意消息類型。
- 使用 Is() 方法可以用來判斷存放的消息類型是否為 typename T。
方法使用:
Address address; cout << "請(qǐng)輸入聯(lián)系人家庭地址: "; string home_address; getline(cin, home_address); address.set_home_address(home_address); cout << "請(qǐng)輸入聯(lián)系人單位地址: "; string unit_address; getline(cin, unit_address); address.set_unit_address(unit_address); google::protobuf::Any * data = people_info_ptr->mutable_data(); data->PackFrom(address); if (people.has_data() && people.data().Is<Address>()) { Address address; people.data().UnpackTo(&address); if (!address.home_address().empty()) { cout << "家庭地址:" << address.home_address() << endl; } if (!address.unit_address().empty()) { cout << "單位地址:" << address.unit_address() << endl; } }
5.oneof 類型
如果消息中有很多可選字段, 并且將來同時(shí)只有一個(gè)字段會(huì)被設(shè)置, 那么就可以使用 oneof 加強(qiáng)這個(gè)行為,也能有節(jié)約內(nèi)存的效果。
oneof other_contact { // repeated string qq = 5; // 不能使用 repeated string qq = 5; string wechat = 6; }
注意:
- 可選字段中的字段編號(hào),不能與非可選字段的編號(hào)沖突。
- 不能在 oneof 中使用 repeated 字段。
- 將來在設(shè)置 oneof 字段中值時(shí),如果將 oneof 中的字段設(shè)置多個(gè),那么只會(huì)保留最后一次設(shè)置的成員,之前設(shè)置的 oneof 成員會(huì)自動(dòng)清除。
- 上述的代碼中,對(duì)于 oneof 字段:會(huì)將 oneof 中的多個(gè)字段定義為一個(gè)枚舉類型。設(shè)置和獲取:對(duì) oneof 內(nèi)的字段進(jìn)行常規(guī)的設(shè)置和獲取即可,但要注意只能設(shè)置一個(gè)。如果設(shè)置多個(gè),那么只會(huì)保留最后一次設(shè)置的成員。清空oneof字段:clear_ 方法,獲取當(dāng)前設(shè)置了哪個(gè)字段:_case 方法。
std::cout << "請(qǐng)選擇要添加的其他聯(lián)系方式(1、qq 2、wechat):"; int other_contact; std::cin>>other_contact; std::cin.ignore(256, '\n'); if(other_contact==1) { std::cout<<"輸入QQ: "; std::string qq; getline(std::cin, qq); people->set_qq(qq); } else if(other_contact==2) { std::cout<<"輸入wechat: "; std::string wechat; getline(std::cin, wechat); people->set_wechat(wechat); } else std::cout<<"輸入錯(cuò)誤!"<<std::endl; switch (people.other_contact_case()) { case PeopleInfo::OtherContactCase::kQq: cout << "qq號(hào): " << people.qq() << endl; break; case PeopleInfo::OtherContactCase::kWeixin: cout << "微信號(hào): " << people.weixin() << endl; break; case PeopleInfo::OtherContactCase::OTHER_CONTACT_NOT_SET: break; }
6.map類型
語(yǔ)法支持創(chuàng)建一個(gè)關(guān)聯(lián)映射字段,也就是可以使用 map 類型去聲明字段類型,格式為:
map<key_type, value_type> map_field = N;
要注意的是:
• key_type 是除了 float 和 bytes 類型以外的任意標(biāo)量類型。 value_type 可以是任意類型。
• map 字段不可以用 repeated 修飾。
• map 中存入的元素是無(wú)序的。
使用:
map<string, string> remark = 7; // 備注
清空map: clear_ 方法
• 設(shè)置和獲?。韩@取方法的方法名稱與小寫字段名稱完全相同。設(shè)置方法為 mutable_ 方法,返回
值為Map類型的指針,這類方法會(huì)為我們開辟好空間,可以直接對(duì)這塊空間的內(nèi)容進(jìn)行修改。
7.默認(rèn)值
反序列化消息時(shí),如果被反序列化的二進(jìn)制序列中不包含某個(gè)字段,反序列化對(duì)象中相應(yīng)字段時(shí),就會(huì)設(shè)置為該字段的默認(rèn)值。不同的類型對(duì)應(yīng)的默認(rèn)值不同:
- 對(duì)于字符串,默認(rèn)值為空字符串。
- 對(duì)于字節(jié),默認(rèn)值為空字節(jié)。
- 對(duì)于布爾值,默認(rèn)值為 false。
- 對(duì)于數(shù)值類型,默認(rèn)值為 0。
- 對(duì)于枚舉,默認(rèn)值是第一個(gè)定義的枚舉值, 必須為 0。
- 對(duì)于消息字段,未設(shè)置該字段。它的取值是依賴于語(yǔ)言。
- 對(duì)于設(shè)置了 repeated 的字段的默認(rèn)值是空的( 通常是相應(yīng)語(yǔ)言的一個(gè)空列表 )。
- 對(duì)于消息字段、oneof字段和any字段,C++ 和 Java 語(yǔ)言中都有 has_ 方法來檢測(cè)當(dāng)前字段是否被設(shè)置。
8.更新消息
更新規(guī)則·
- 禁止修改任何已有字段的字段編號(hào)。
- 若是移除老字段,要保證不再使用移除字段的字段編號(hào)。正確的做法是保留字段編號(hào)(reserved),以確保該編號(hào)將不能被重復(fù)使用。不建議直接刪除或注釋掉字段。
- int32, uint32, int64, uint64 和 bool 是完全兼容的??梢詮倪@些類型中的一個(gè)改為另一個(gè),而不破壞前后兼容性。若解析出來的數(shù)值與相應(yīng)的類型不匹配,會(huì)采用與 C++ 一致的處理方案(例如,若將 64 位整數(shù)當(dāng)做 32 位進(jìn)行讀取,它將被截?cái)酁?32 位)。
- sint32 和 sint64 相互兼容但不與其他的整型兼容。
- string 和 bytes 在合法 UTF-8 字節(jié)前提下也是兼容的。
- bytes 包含消息編碼版本的情況下,嵌套消息與 bytes 也是兼容的。
- fixed32 與 sfixed32 兼容, fixed64 與 sfixed64兼容。
- enum 與 int32,uint32, int64 和 uint64 兼容(注意若值不匹配會(huì)被截?cái)啵5⒁猱?dāng)反序列化消息時(shí)會(huì)根據(jù)語(yǔ)言采用不同的處理方案:例如,未識(shí)別的 proto3 枚舉類型會(huì)被保存在消息中,但是當(dāng)消息反序列化時(shí)如何表示是依賴于編程語(yǔ)言的。整型字段總是會(huì)保持其的值。
- oneof:
- 將一個(gè)單獨(dú)的值更改為 新 oneof 類型成員之一是安全和二進(jìn)制兼容的。若確定沒有代碼一次性設(shè)置多個(gè)值那么將多個(gè)字段移入一個(gè)新 oneof 類型也是可行的。將任何字段移入已存在的 oneof 類型是不安全的。
保留字段:reserved
- 如果通過 刪除 或 注釋掉 字段來更新消息類型,未來的用戶在添加新字段時(shí),有可能會(huì)使用以前已經(jīng)存在,但已經(jīng)被刪除或注釋掉的字段編號(hào)。將來使用該 .proto 的舊版本時(shí)的程序會(huì)引發(fā)很多問題:數(shù)據(jù)損壞、隱私錯(cuò)誤等等。
- 確保不會(huì)發(fā)生這種情況的一種方法是:使用 reserved 將指定字段的編號(hào)或名稱設(shè)置為保留項(xiàng) 當(dāng)我們?cè)偈褂眠@些編號(hào)或名稱時(shí),protocol buffer 的編譯器將會(huì)警告這些編號(hào)或名稱不可用。
未知字段
未知字段:解析結(jié)構(gòu)良好的 protocol buffer 已序列化數(shù)據(jù)中的未識(shí)別字段的表示方式。例如,當(dāng)舊程序解析帶有新字段的數(shù)據(jù)時(shí),這些新字段就會(huì)成為舊程序的未知字段。
本來,proto3 在解析消息時(shí)總是會(huì)丟棄未知字段,但在 3.5 版本中重新引入了對(duì)未知字段的保留機(jī)制。所以在 3.5 或更高版本中,未知字段在反序列化時(shí)會(huì)被保留,同時(shí)也會(huì)包含在序列化的結(jié)果中。
9.UnknownFieldSet 類介紹
- UnknownFieldSet 包含在分析消息時(shí)遇到但未由其類型定義的所有字段。
- 若要將 UnknownFieldSet 附加到任何消息,請(qǐng)調(diào)用 Reflection::GetUnknownFields()。
- 類定義在 unknown_field_set.h 中。
10.UnknownField 類介紹
- 表示未知字段集中的一個(gè)字段。
- 類定義在 unknown_field_set.h 中。
使用:
const Reflection *reflection = PeopleInfo::GetReflection(); const UnknownFieldSet &unknowSet = reflection->GetUnknownFields(people); for (int j = 0; j < unknowSet.field_count(); j++) { const UnknownField &unknow_field = unknowSet.field(j); cout << "未知字段" << j + 1 << ":" << " 字段編號(hào): " << unknow_field.number() << " 類型: " << unknow_field.type(); switch (unknow_field.type()) { case UnknownField::Type::TYPE_VARINT: cout << " 值: " << unknow_field.varint() << endl; break; case UnknownField::Type::TYPE_LENGTH_DELIMITED: cout << " 值: " << unknow_field.length_delimited() << endl; break; } }
11.前后兼容性
前后兼容的作用,當(dāng)我們維護(hù)一個(gè)很大的分布式系統(tǒng)時(shí),由于無(wú)法同時(shí)升級(jí)說有模塊,為了在保證升級(jí)過程中,整個(gè)系統(tǒng)能夠盡可能不受影響,就需要盡量保證通訊協(xié)議的“向后兼容”或“向前兼容”。
12.選項(xiàng) option
proto 文件中可以聲明許多選項(xiàng),使用 option 標(biāo)注。選項(xiàng)能影響 proto 編譯器的某些處理方式。
選項(xiàng)的完整列表在google/protobuf/descriptor.proto中定義。文件分為字段級(jí),消息級(jí),文件級(jí),但并沒有所有的選項(xiàng)能作用于所有類型,常見舉例
optimize_for:為文件選項(xiàng),可以設(shè)置 protoc 編譯器的優(yōu)化級(jí)別,分別為 SPEED 、
CODE_SIZE 、LITE_RUNTIME 。受該選項(xiàng)影響,設(shè)置不同的優(yōu)化級(jí)別,編譯 .proto 文件后生成的代碼內(nèi)容不同。
- SPEED:protoc編譯器將生成的代碼是高度優(yōu)化的,代碼運(yùn)行效率高,但生成的代碼更占空間,SPEED為默認(rèn)選項(xiàng)。
- CODE_SIZE : proto 編譯器將生成最少的類,會(huì)占用更少的空間,是依賴基于反射的代碼來實(shí)現(xiàn)序列化、反序列化和各種其他操作。但和SPEED 恰恰相反,它的代碼運(yùn)行效率較低。這種方式適合用在包含大量的.proto文件,但并不盲目追求速度的應(yīng)用中。
- LITE_RUNTIME : 生成的代碼執(zhí)行效率高,同時(shí)生成代碼編譯后的所占用的空間也是非常少。這是以犧牲Protocol Buffer提供的反射功能為代價(jià)的,僅僅提供 encoding+序列化 功能,所以我們?cè)阪溄?BP 庫(kù)時(shí)僅需鏈接libprotobuf-lite,而非libprotobuf。這種模式通常用于資源有限的平臺(tái),例如移動(dòng)手機(jī)平臺(tái)中。
- allow_alias : 允許將相同的常量值分配給不同的枚舉常量,用來定義別名。該選項(xiàng)為枚舉選項(xiàng)。
本地版本代碼實(shí)現(xiàn):
四:網(wǎng)絡(luò)版本通訊錄的實(shí)現(xiàn)
Protobuf 還常用于通訊協(xié)議、服務(wù)端數(shù)據(jù)交換場(chǎng)景。那么在這個(gè)示例中,我們將實(shí)現(xiàn)一個(gè)網(wǎng)絡(luò)版本通訊錄,模擬實(shí)現(xiàn)客戶端與服務(wù)端的交互,通過 Protobuf 來實(shí)現(xiàn)各端之間的協(xié)議序列化。
1.環(huán)境搭建
Httplib 庫(kù):cpp-httplib 是個(gè)開源的庫(kù),是一個(gè)c++封裝的http庫(kù),使用這個(gè)庫(kù)可以在linux、
windows平臺(tái)下完成http客戶端、http服務(wù)端的搭建。使用起來非常方便,只需要包含頭文件
httplib.h 即可。編譯程序時(shí),需要帶上 -lpthread 選項(xiàng)。
鏡像倉(cāng)庫(kù):weixin_30886717 / cpp-httplib · GitCodecentos 下編寫的注意事項(xiàng):
如果使用 centOS 環(huán)境,yum源帶的 g++ 最新版本是4.8.5,發(fā)布于2015年,年代久遠(yuǎn)。編譯該項(xiàng)目會(huì)出現(xiàn)異常。將 gcc/g++ 升級(jí)為更高版本可解決問題。
實(shí)現(xiàn):
// 自定義異常類 class ContactException { private: std::string message; public: ContactException(std::string str = "A problem") : message{str} {} std::string what() const { return message; } }; #include <iostream> #include "contact.pb.h" #include "ContactException.h" void menu() { std::cout << "-----------------------------------------------------" << std::endl << "--------------- 請(qǐng)選擇對(duì)通訊錄的操作 ----------------" << std::endl << "------------------ 1、新增聯(lián)系人 --------------------" << std::endl << "------------------ 2、刪除聯(lián)系人 --------------------" << std::endl << "------------------ 3、查看聯(lián)系人列表 ----------------" << std::endl << "------------------ 4、查看聯(lián)系人詳細(xì)信息 ------------" << std::endl << "------------------ 0、退出 --------------------------" << std::endl << "-----------------------------------------------------" << std::endl; } int main() { enum OPERATE { QUIT = 0, ADD, DEL, FIND_ALL, FIND_ONE }; while (true) { menu(); std::cout << "---> 請(qǐng)選擇:"; int choose; std::cin >> choose; std::cin.ignore(256, '\n'); try { switch (choose) { case OPERATE::ADD: contactsServer.addContact(); break; case OPERATE::DEL: contactsServer.delContact(); break; case OPERATE::FIND_ALL: contactsServer.findContacts(); break; case OPERATE::FIND_ONE: contactsServer.findContact(); break; case 0: std::cout << "---> 程序已退出" << std::endl; return 0; default: std::cout << "---> 無(wú)此選項(xiàng),請(qǐng)重新選擇!" << std::endl; break; } } catch (const ContactException &e) { std::cerr << "---> 操作通訊錄時(shí)發(fā)現(xiàn)異常?。。? << std::endl << "---> 異常信息:" << e.what() << std::endl; } catch (const std::exception &e) { std::cerr << "---> 操作通訊錄時(shí)發(fā)現(xiàn)異常?。。? << std::endl << "---> 異常信息:" << e.what() << std::endl; } } } class ContactsServer { public: void addContact(); void delContact(); void findContacts(); void findContact(); private: void buildAddContactRequest(add_contact_req::AddContactRequest* req); void printFindOneContactResponse(find_one_contact_resp::FindOneContactResponse& resp); void printFindAllContactsResponse(find_all_contacts_resp::FindAllContactsResponse& resp); };
五:總結(jié):
到此這篇關(guān)于C/C++ProtoBuf使用的文章就介紹到這了,更多相關(guān)C++ProtoBuf使用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C語(yǔ)言實(shí)現(xiàn)簡(jiǎn)單計(jì)算器功能(1)
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)簡(jiǎn)單計(jì)算器功能的第一部分,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-02-02windows系統(tǒng)下C++調(diào)用matlab程序的方法詳解
這篇文章主要給大家介紹了關(guān)于在windows系統(tǒng)下C++調(diào)用matlab程序的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用C++具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面跟著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-08-08C++簡(jiǎn)易版Tensor實(shí)現(xiàn)方法詳解
這篇文章主要介紹了C++簡(jiǎn)易版Tensor的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值2022-08-08C語(yǔ)言實(shí)現(xiàn)學(xué)生選課系統(tǒng)完整版
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)學(xué)生選課系統(tǒng)的完整版,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02C++實(shí)現(xiàn)簡(jiǎn)易的彈球小游戲
這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)簡(jiǎn)易的彈球小游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10C++?Socket實(shí)現(xiàn)TCP與UDP網(wǎng)絡(luò)編程
本文主要介紹了C++?Socket實(shí)現(xiàn)TCP與UDP網(wǎng)絡(luò)編程,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01