C/C++ProtoBuf使用小结
作者:函数指针
一.什么是ProtoBuf
1.序列化和反序列化概念
序列化:把对象转变为字节序列的过程,称为系列化。
反序列化:把字节序列的内容恢复为对象的过程,称为反序列化。
2.什么情况下需要序列化和反序列化
存储数据:将内存中的对象状态保存在文件中或存储在数据库中时。
网络传输:网络传输数据时,无法直接传输对象,需要在传输前序列化,传输完成后反 序列化成对象,就像学习Socket编程中发送与接收时。
3.如何实现序列化
xml,json,protoBuf这三种工具都可以实现序列化和反序列化
4.ProtoBuf是什么
ProtoBuf是让数据结构序列化的方法,具有以下特点:
- 语言无关、平台无关:即 ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台。
- 高效:即比 XML 更小、更快、更为简单。
- 扩展性、兼容性好:你可以更新数据结构,而不影响和破坏原有的旧程序。
二.ProtoBuf简单语法知识
1.文件规范
创建一个ProtoBuf文件,后缀一定以.proto结尾,文件命名全小写字母,多个字母之间以_为分隔符,添加注释的方法和C/C++的一毛一样。
2.ProtoBuf语法分类
ProtoBuf有对个版本,在这里我们使用最新的版本,protobuf3的语法,简称proto3,它是最新的ProtoBuf语法版本。proto3 简化了 ProtoBuf 语言,既易于使用,又可以在更广泛的编程语言中使用。它允许你使用 Java,C++,Python等多种语言生成 protocol buffer 代码。
3.ProtoBuf3语法
syntax
我们在创建一个文件时,一定要在文件开头致命你所使用的语法,如果没有指定,默认使用Protobuf2的语法来使用,
比如:
syntax = "proto3";
syntax为指定语法,一定要有,就像函数中一定要指明函数返回类型一样。
package(声明符)
package 是一个可选的声明符,能表示 .proto 文件的命名空间,在项目中要有唯一性。它的作用是为了避免我们定义的消息出现冲突,就像c++中的namespace一样,指定一个域,防止冲突。
比如:
package person;
message(定义消息)
消息(message):要定义的结构化对象,可以给这个结构化对象中定义对用的属性内容,就像C//C++中的结构体一样,只能够定义对象。
这里强调下为什么要定义对象:
网络传输中,需要传输双方定制协议,定制协议就是定制结构体或者结构化数据,比如:Tcp,Udp。而且将数据存储在数据库时,需将数据统一为对象组织起来,在进行存储。
所以ProtoBuf就是以message的方式支持我们定义协议字段的。比如:
syntax="proto3"; package=package; message people { ; }
定义字段消息
在 message 中我们可以定义其属性字段,字段定义格式为:字段类型 字段名 = 字段唯一编号,比如:
syntax="proto3"; package=package; message people { int32 age=1; }
int32为字段类型 age为字段名 1为字段唯一编号
下面是protobuf创建类型及所对应的C/C++类型:
注:[1] 变长编码是指:经过protobuf 编码后,原本4字节或8字节的数可能会被变为其他字节数。
比如:
syntax="proto3" package sss message person { string name=1; int32 age=2; string sex=3; }
注意:字段唯一编号范围:
2^0~2^29-1,其中,19000~19999不可用,在 Protobuf 协议的实现中,对这些数进行了预留。如果非要在.proto文件中使用这些预留标识号,例如将 name 字段的编号设置为19000,编译时就会报警。值得一提的是,范围为 1 ~ 15 的字段编号需要一个字节进行编码, 16 ~ 2047 内的数字需要两个字节进行编码。编码后的字节不仅只包含了编号,还包含了字段类型。所以 1 ~ 15 要用来标记出现非常频繁的字段,要为将来有可能添加的、频繁出现的字段预留一些出来。
通过上面的学习,我们来实现一个简单的test.proto文件,
syntax="proto3"; package sss; message person { string name=1; int32 age=2; string sex=3; }
编辑protobuf命令如下:proto --cpp_out. [文件名]
我们使用的是C++语言,所以编译之后生成两个文件:test.pb.h和test.pb.cc。
对于编译生成的 C++ 代码,包含了以下内容 :
- 对于每个 message ,都会生成一个对应的消息类。
- 在消息类中,编译器为每个字段提供了获取和设置方法,以及一下其他能够操作字段的方法。
- 编辑器会针对于每个 .proto 文件生成.h 和 .cc 文件,分别用来存放类的声明与类的实现。
代码过长,就不展示了,感兴趣的自己做下!!!
编译命令行格式
protoc [--proto_path=IMPORT_PATH] --cpp_out=DST_DIR path/to/file.proto
protoc:是protobuf提供的命令行编辑工具。
--proto_path: 指定被编辑的所在目录,可多次指定,可简写为-I。如不指定,则在当前目录下进行搜索。
--cpp_out=: 指定编译后的文件为c++文件。
DST_DIR; 文件路径
path/to/file.proto:要编译的文件。
上述的例子中,每个字段都有设置和获取的方法,getter 的名称与小写字段完全相同,setter 方法以 set_ 开头,每个字段都有一个clear_的方法,可以将字段设置为empty状态。比如:
#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中,提供了读写消息实例的方法,包括序列化方法和反序列化方法。
class MessageLite { public: //序列化: bool SerializeToOstream(ostream* output) const; // 将序列化后数据写入文件 //流 bool SerializeToArray(void *data, int size) const; bool SerializeToString(string* output) const; //反序列化: bool ParseFromIstream(istream* input); // 从流中读取数据,再进行反序列化 //动作 bool ParseFromArray(const void* data, int size); bool ParseFromString(const string& data); };
注意:
序列化的方法为二进制方法,非文本格式;以上三种序列化的方法没有本质区别,只是序列化之后输出的格式不同,可以提供不同场景使用,序列化的 API 函数均为const成员函数,因为序列化不会改变类对象的内容, 而是将序列化的结果保存到函数入参指定的地址中。
更多详细API函数参考:message.h | Protocol Buffers Documentation (protobuf.dev)
序列化和反序列化的使用:看结果
#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 << "序列化联系人失败." << endl; } // 打印序列化结果 cout << "序列化后的 people_str: " << people_str << endl; if(!p1.SerializePartialToString(&people_str)) { cout<<"反序列化失败:"<<endl; } cout<<"反序列化成功:"<<endl<<people_str<<endl; return 0; }
这就是简单的Protobuf的应用。
接下来是ProtoBuf的语法详解。
三.ProtoBuf语法详解
在这部分,我们使用一个简单的项目推进的方式来讲解语法内容,通过一个通讯录的实现,完成我们从最基础的内容到网络版本的知识。
1.字段规则
singular :消息中可以包含该字段零次或一次(不超过一次)。 proto3 语法中,字段默认使用该规则,可以不用设置。
repeated :消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。可以理解为定义了一个数组。
比如:
syntax="proto3"; package contacts2; message person { string name=1; int32 age=2; repeated string phone_num=3; }
2.消息类型的定义与使用
在单个.proto文件中,可以定义多个message,也可以嵌套定义,每个message中的字段编号可以重复,并且,消息字段也可以作为字段类型来使用,比如:
syntax="proto3"; package contacts2; // message Phone{ // string phone_num=1; // } message person { string name=1; int32 age=2; //第一种,单独写法 //repeated Phone=3; //第二种,嵌套写法 message Phone{ string phone_num=1; } repeated Phone=3; }
也可以将其他.proto文件中定义的消息导入并使用,但一定要加import 比如:
syntax="proto3"; package contacts2; import "Photo.proto";//将其他文件中的消息字段导入 message person { string name=1; int32 age=2; //第三种 repeated Phone.Phone phone=3; } syntax="proto3"; package Phone; message Phone{ string phone_num=1; }
注:在 proto3 文件中可以导入 proto2 消息类型并使用它们,反之亦然
编译
我们可以将以上的内容稍加修改,变为我们的第一版通讯录:
syntax="proto3"; package contacts2; //import "Photo.proto";//将其他文件中的消息字段导入 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 会将老版本的代码覆盖掉,由于代码过长,就不展示啦!!!
上述的代码中:
- 每个字段都有一个 clear_ 方法,可以将字段重新设置回 empty 状态。
- 每个字段都有设置和获取的方法, 获取方法的方法名称与小写字段名称完全相同。但如果是消息类型的字段,其设置方法为 mutable_ 方法,返回值为消息类型的指针,这类方法会为我们开辟好空间,可以直接对这块空间的内容进行修改。
- 对于使用 repeated 修饰的字段,也就是数组类型,pb 为我们提供了 add_ 方法来新增一个值,并且提供了 _size 方法来判断数组存放元素的个数。
我们可以对编译好的代码试用一下:
#include <iostream> #include <fstream> #include "contact.pb.h" using namespace std; using namespace contacts; void AddPeopleInfo(PeopleInfo *people_info_ptr) { cout << "-------------新增联系人-------------" << endl; cout << "请输入联系人姓名: "; string name; getline(cin, name); people_info_ptr->set_name(name);//输入用set_函数 cout << "请输入联系人年龄: "; int age; cin >> age; people_info_ptr->set_age(age); cin.ignore(256, '\n'); for (int i = 1;; i++) { cout << "请输入联系人电话" << i << "(只输入回车完成电话新增): "; string number; getline(cin, number); if (number.empty()) { break; } //number需要添加才可以用 PeopleInfo_Phone *phone = people_info_ptr->add_phone(); phone->set_number(number); } cout << "-----------添加联系人成功-----------" << 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; } // 新增一个联系人 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类型的使用
语法支持我们定义枚举类型来使用,我们可以定义一个enum类型的使用,
syntax="proto3"; package Phone; message{ enum PhoneType{ MP=0;//移动电话 TEL=1;//固定电话 } }
要注意枚举类型的定义有以下几种规则:
1. 0 值常量必须存在,且要作为第一个元素。这是为了与 proto2 的语义兼容:第一个元素作为默认值,且值为 0。
2. 枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)。
3. 枚举的常量值在 32 位整数的范围内。但因负值无效因而不建议使用(与编码规则有关)。定义时注意:
将两个 ‘具有相同枚举值名称’ 的枚举类型放在单个 .proto 文件下测试时,编译后会报错:某某某常量已经被定义!所以这里要注意:
• 同级(同层)的枚举类型,各个枚举类型中的常量不能重名。
• 单个 .proto 文件下,最外层枚举类型和嵌套枚举类型,不算同级。
• 多个 .proto 文件下,若一个文件引入了其他文件,且每个文件都未声明 package,每个 proto 文件中的枚举类型都在最外层,算同级。
• 多个 .proto 文件下,若一个文件引入了其他文件,且每个文件都声明了 package,不算同级。
下面我们来使用一下:
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; // 移动电话 TEL = 1; // 固定电话 } PhoneType type=2; } //多组电话 repeated Phone phone=3; }
编译生成后的文件:对于在.proto文件中定义的枚举类型,编译生成的代码中会含有与之对应的枚举类型、校验枚举值是否有效的方法 _IsValid、以及获取枚举值名称的方法 _Name。对于使用了枚举类型的字段,包含设置和获取字段的方法,已经清空字段的方法clear_;
4.any类型
字段还可以声明为 Any 类型,可以理解为泛型类型。使用时可以在 Any 中存储任意消息类型。Any 类型的字段也用 repeated 来修饰,Any 类型是 google 已经帮我们定义好的类型,在安装 ProtoBuf 时,其中的 include 目录下查找所有google 已经定义好的 .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; // 移动电话 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; }
上述的代码中,对于 Any 类型字段:
设置和获取:获取方法的方法名称与小写字段名称完全相同。设置方法可以使用 mutable_ 方
法,返回值为Any类型的指针,这类方法会为我们开辟好空间,可以直接对这块空间的内容进行
修改;之前说过,我们可以在 Any 字段中存储任意消息类型,这就要涉及到任意消息类型 和 Any 类型的互转。这部分代码就在 Google为我们写好的头文件 any.pb.h 中。使用方法:
- 使用 PackFrom() 方法可以将任意消息类型转为 Any 类型。
- 使用 UnpackTo() 方法可以将 Any 类型转回之前设置的任意消息类型。
- 使用 Is() 方法可以用来判断存放的消息类型是否为 typename T。
方法使用:
Address address; cout << "请输入联系人家庭地址: "; string home_address; getline(cin, home_address); address.set_home_address(home_address); cout << "请输入联系人单位地址: "; 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 类型
如果消息中有很多可选字段, 并且将来同时只有一个字段会被设置, 那么就可以使用 oneof 加强这个行为,也能有节约内存的效果。
oneof other_contact { // repeated string qq = 5; // 不能使用 repeated string qq = 5; string wechat = 6; }
注意:
- 可选字段中的字段编号,不能与非可选字段的编号冲突。
- 不能在 oneof 中使用 repeated 字段。
- 将来在设置 oneof 字段中值时,如果将 oneof 中的字段设置多个,那么只会保留最后一次设置的成员,之前设置的 oneof 成员会自动清除。
- 上述的代码中,对于 oneof 字段:会将 oneof 中的多个字段定义为一个枚举类型。设置和获取:对 oneof 内的字段进行常规的设置和获取即可,但要注意只能设置一个。如果设置多个,那么只会保留最后一次设置的成员。清空oneof字段:clear_ 方法,获取当前设置了哪个字段:_case 方法。
std::cout << "请选择要添加的其他联系方式(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<<"输入错误!"<<std::endl; switch (people.other_contact_case()) { case PeopleInfo::OtherContactCase::kQq: cout << "qq号: " << people.qq() << endl; break; case PeopleInfo::OtherContactCase::kWeixin: cout << "微信号: " << people.weixin() << endl; break; case PeopleInfo::OtherContactCase::OTHER_CONTACT_NOT_SET: break; }
6.map类型
语法支持创建一个关联映射字段,也就是可以使用 map 类型去声明字段类型,格式为:
map<key_type, value_type> map_field = N;
要注意的是:
• key_type 是除了 float 和 bytes 类型以外的任意标量类型。 value_type 可以是任意类型。
• map 字段不可以用 repeated 修饰。
• map 中存入的元素是无序的。
使用:
map<string, string> remark = 7; // 备注
清空map: clear_ 方法
• 设置和获取:获取方法的方法名称与小写字段名称完全相同。设置方法为 mutable_ 方法,返回
值为Map类型的指针,这类方法会为我们开辟好空间,可以直接对这块空间的内容进行修改。
7.默认值
反序列化消息时,如果被反序列化的二进制序列中不包含某个字段,反序列化对象中相应字段时,就会设置为该字段的默认值。不同的类型对应的默认值不同:
- 对于字符串,默认值为空字符串。
- 对于字节,默认值为空字节。
- 对于布尔值,默认值为 false。
- 对于数值类型,默认值为 0。
- 对于枚举,默认值是第一个定义的枚举值, 必须为 0。
- 对于消息字段,未设置该字段。它的取值是依赖于语言。
- 对于设置了 repeated 的字段的默认值是空的( 通常是相应语言的一个空列表 )。
- 对于消息字段、oneof字段和any字段,C++ 和 Java 语言中都有 has_ 方法来检测当前字段是否被设置。
8.更新消息
更新规则·
- 禁止修改任何已有字段的字段编号。
- 若是移除老字段,要保证不再使用移除字段的字段编号。正确的做法是保留字段编号(reserved),以确保该编号将不能被重复使用。不建议直接删除或注释掉字段。
- int32, uint32, int64, uint64 和 bool 是完全兼容的。可以从这些类型中的一个改为另一个,而不破坏前后兼容性。若解析出来的数值与相应的类型不匹配,会采用与 C++ 一致的处理方案(例如,若将 64 位整数当做 32 位进行读取,它将被截断为 32 位)。
- sint32 和 sint64 相互兼容但不与其他的整型兼容。
- string 和 bytes 在合法 UTF-8 字节前提下也是兼容的。
- bytes 包含消息编码版本的情况下,嵌套消息与 bytes 也是兼容的。
- fixed32 与 sfixed32 兼容, fixed64 与 sfixed64兼容。
- enum 与 int32,uint32, int64 和 uint64 兼容(注意若值不匹配会被截断)。但要注意当反序列化消息时会根据语言采用不同的处理方案:例如,未识别的 proto3 枚举类型会被保存在消息中,但是当消息反序列化时如何表示是依赖于编程语言的。整型字段总是会保持其的值。
- oneof:
- 将一个单独的值更改为 新 oneof 类型成员之一是安全和二进制兼容的。若确定没有代码一次性设置多个值那么将多个字段移入一个新 oneof 类型也是可行的。将任何字段移入已存在的 oneof 类型是不安全的。
保留字段:reserved
- 如果通过 删除 或 注释掉 字段来更新消息类型,未来的用户在添加新字段时,有可能会使用以前已经存在,但已经被删除或注释掉的字段编号。将来使用该 .proto 的旧版本时的程序会引发很多问题:数据损坏、隐私错误等等。
- 确保不会发生这种情况的一种方法是:使用 reserved 将指定字段的编号或名称设置为保留项 当我们再使用这些编号或名称时,protocol buffer 的编译器将会警告这些编号或名称不可用。
未知字段
未知字段:解析结构良好的 protocol buffer 已序列化数据中的未识别字段的表示方式。例如,当旧程序解析带有新字段的数据时,这些新字段就会成为旧程序的未知字段。
本来,proto3 在解析消息时总是会丢弃未知字段,但在 3.5 版本中重新引入了对未知字段的保留机制。所以在 3.5 或更高版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果中。
9.UnknownFieldSet 类介绍
- UnknownFieldSet 包含在分析消息时遇到但未由其类型定义的所有字段。
- 若要将 UnknownFieldSet 附加到任何消息,请调用 Reflection::GetUnknownFields()。
- 类定义在 unknown_field_set.h 中。
10.UnknownField 类介绍
- 表示未知字段集中的一个字段。
- 类定义在 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 << ":" << " 字段编号: " << 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.前后兼容性
前后兼容的作用,当我们维护一个很大的分布式系统时,由于无法同时升级说有模块,为了在保证升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的“向后兼容”或“向前兼容”。
12.选项 option
proto 文件中可以声明许多选项,使用 option 标注。选项能影响 proto 编译器的某些处理方式。
选项的完整列表在google/protobuf/descriptor.proto中定义。文件分为字段级,消息级,文件级,但并没有所有的选项能作用于所有类型,常见举例
optimize_for:为文件选项,可以设置 protoc 编译器的优化级别,分别为 SPEED 、
CODE_SIZE 、LITE_RUNTIME 。受该选项影响,设置不同的优化级别,编译 .proto 文件后生成的代码内容不同。
- SPEED:protoc编译器将生成的代码是高度优化的,代码运行效率高,但生成的代码更占空间,SPEED为默认选项。
- CODE_SIZE : proto 编译器将生成最少的类,会占用更少的空间,是依赖基于反射的代码来实现序列化、反序列化和各种其他操作。但和SPEED 恰恰相反,它的代码运行效率较低。这种方式适合用在包含大量的.proto文件,但并不盲目追求速度的应用中。
- LITE_RUNTIME : 生成的代码执行效率高,同时生成代码编译后的所占用的空间也是非常少。这是以牺牲Protocol Buffer提供的反射功能为代价的,仅仅提供 encoding+序列化 功能,所以我们在链接 BP 库时仅需链接libprotobuf-lite,而非libprotobuf。这种模式通常用于资源有限的平台,例如移动手机平台中。
- allow_alias : 允许将相同的常量值分配给不同的枚举常量,用来定义别名。该选项为枚举选项。
本地版本代码实现:
四:网络版本通讯录的实现
Protobuf 还常用于通讯协议、服务端数据交换场景。那么在这个示例中,我们将实现一个网络版本通讯录,模拟实现客户端与服务端的交互,通过 Protobuf 来实现各端之间的协议序列化。
1.环境搭建
Httplib 库:cpp-httplib 是个开源的库,是一个c++封装的http库,使用这个库可以在linux、
windows平台下完成http客户端、http服务端的搭建。使用起来非常方便,只需要包含头文件
httplib.h 即可。编译程序时,需要带上 -lpthread 选项。
镜像仓库:weixin_30886717 / cpp-httplib · GitCodecentos 下编写的注意事项:
如果使用 centOS 环境,yum源带的 g++ 最新版本是4.8.5,发布于2015年,年代久远。编译该项目会出现异常。将 gcc/g++ 升级为更高版本可解决问题。
实现:
// 自定义异常类 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 << "--------------- 请选择对通讯录的操作 ----------------" << std::endl << "------------------ 1、新增联系人 --------------------" << std::endl << "------------------ 2、删除联系人 --------------------" << std::endl << "------------------ 3、查看联系人列表 ----------------" << std::endl << "------------------ 4、查看联系人详细信息 ------------" << std::endl << "------------------ 0、退出 --------------------------" << std::endl << "-----------------------------------------------------" << std::endl; } int main() { enum OPERATE { QUIT = 0, ADD, DEL, FIND_ALL, FIND_ONE }; while (true) { menu(); std::cout << "---> 请选择:"; 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 << "---> 无此选项,请重新选择!" << std::endl; break; } } catch (const ContactException &e) { std::cerr << "---> 操作通讯录时发现异常!!!" << std::endl << "---> 异常信息:" << e.what() << std::endl; } catch (const std::exception &e) { std::cerr << "---> 操作通讯录时发现异常!!!" << 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); };
五:总结:
到此这篇关于C/C++ProtoBuf使用的文章就介绍到这了,更多相关C++ProtoBuf使用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!