一.什么是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 sssmessage 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 #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 #include #include "contact.pb.h"using namespace std;using namespace contacts;void AddPeopleInfo(PeopleInfo *people_info_ptr){cout << "-------------新增联系人-------------" << endl;cout <set_name(name);//输入用set_函数cout <> age;people_info_ptr->set_age(age);cin.ignore(256, '\n');for (int i = 1;; i++){cout << "请输入联系人电话" << i <add_phone();phone->set_number(number);}cout << "-----------添加联系人成功-----------" << endl;}int main(){Contacts contacts;// 先读取已存在的 contactsfstream 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());//向磁盘文件写入新的 contactsfstream 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;// 不能使用 repeatedstring qq = 5;string wechat = 6;}//备注信息map 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 <mutable_data();data->PackFrom(address);if (people.has_data() && people.data().Is()) {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;// 不能使用 repeatedstring qq = 5;string wechat = 6; }

注意:

  • 可选字段中的字段编号,不能与非可选字段的编号冲突。
  • 不能在 oneof 中使用 repeated 字段。
  • 将来在设置 oneof 字段中值时,如果将 oneof 中的字段设置多个,那么只会保留最后一次设置的成员,之前设置的 oneof 成员会自动清除。
  • 上述的代码中,对于 oneof 字段:会将 oneof 中的多个字段定义为一个枚举类型。设置和获取:对 oneof 内的字段进行常规的设置和获取即可,但要注意只能设置一个。如果设置多个,那么只会保留最后一次设置的成员。清空oneof字段:clear_ 方法,获取当前设置了哪个字段:_case 方法。
std::cout <>other_contact;std::cin.ignore(256, '\n');if(other_contact==1){std::cout<set_qq(qq);}else if(other_contact==2){std::cout<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 map_field = N;
要注意的是:
• key_type 是除了 float 和 bytes 类型以外的任意标量类型。 value_type 可以是任意类型。
• map 字段不可以用 repeated 修饰。
• map 中存入的元素是无序的。

使用:

map 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 · GitCode

centos 下编写的注意事项:
如果使用 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 #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);};

五:总结: