基于protobuf实现网络版本通讯录(protobuf 0基础可看)
基于protobuf实现网络版本通讯录(protobuf 0基础可看)
- 序列化与反序列化
- ProtoBuf特点
- ProtoBuf使用流程
- 快速上手
- 创建.proto后缀文件
- 指定proto3语法
- package声明符
- 定义消息(message)
- 定义消息字段
- Protobuf 类型对照表
- 标量类型
- enum类型
- Any类型
- oneof类型
- map 类型
- 默认值
- 字段编号规则
- proto3语法
- 字段规则
- 消息类型的定义与使用
- 定义
- 使用
- protoc指令
- 消息更新规则
- 保留字段 reserved
- 获取未知字段
- 如何获取未知字段
- option选项
- 选项分类
- 常用选项列举
- 网络版通讯录实战
- 环境搭建
- 大致流程
- 添加联系人实现
- 客户端代码实现
- 服务端代码实现
序列化与反序列化
- 序列化:把对象转换为字节序列的过程 称为对象的序列化。
- 反序列化:把字节序列恢复为对象的过程 称为对象的反序列化。
为何要进行序列化反序列化?
- 统一双发通信格式,可以两方都认识。因为在开发中服务器与客户端的开发语言可能就不是一种语言。假设发送一个cpp结构体,对方语言可能无法识别。因此需要进行序列化反序列化。
什么情况下需要序列化?
- 网络传输:网络传输:网络直接传输数据,但是无法直接传输对象,所以要在传输前序列化,传输完成后反序列化成对象。
- 持久化存储数据:当你想把的内存中的对象状态保存到文件中或者数据库时候。
ProtoBuf特点
- 语言无关、平台无关:即 ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台。
- 高效:即比 XML 更小、更快、更为简单。
- 扩展性、兼容性好:你可以更新数据结构,而不影响和破坏原有的旧程序。
ProtoBuf使用流程
- 编写 .proto 文件,目的是为了定义结构对象(message)及属性内容。
- 使用 protoc 编译器编译 .proto 文件,生成一系列接口代码,存放在新生成头文件和源文件中。
- 依赖生成的接口,将编译生成的头文件包含进我们的代码中,实现对 .proto 文件中定义的字段进行设置和获取,和对 message 对象进行序列化和反序列化。
快速上手
创建.proto后缀文件
指定proto3语法
/* 后续引入的都是proto3语法*/
syntax = "proto3";
package声明符
/* 相当于我们c++的命名空间*/
package contacts;
定义消息(message)
消息格式:
message 消息类型名{}
消息类型命名规范:使⽤驼峰命名法,⾸字⺟⼤写。
定义消息字段
在 message 中我们可以定义其属性字段,字段定义格式为:字段类型 字段名 = 字段唯⼀编号;
- 字段名称命名规范:全⼩写字⺟,多个字⺟之间⽤ _ 连接。
- 字段类型分为:标量数据类型 和 特殊类型(包括枚举、其他消息类型等)。
- 字段唯⼀编号:⽤来标识字段,⼀旦开始使⽤就不能够再改变。
根据您提供的内容,我将其整理为Markdown表格格式:
Protobuf 类型对照表
标量类型
.proto Type | Notes | C++ Type |
---|---|---|
double | double | |
float | float | |
int32 | 使用变长编码[1]。负数的编码效率较低——若字段可能为负值,应使用 sint32 代替。 | int32 |
int64 | 使用变长编码[1]。负数的编码效率较低——若字段可能为负值,应使用 sint64 代替。 | int64 |
uint32 | 使用变长编码[1]。 | uint32 |
uint64 | 使用变长编码[1]。 | uint64 |
sint32 | 使用变长编码[1]。符号整型。负值的编码效率高于常规的 int32 类型。 | int32 |
sint64 | 使用变长编码[1]。符号整型。负值的编码效率高于常规的 int64 类型。 | int64 |
fixed32 | 定长 4 字节。若值常大于222^2228^88 则会比 uint32 更高效。 | uint32 |
fixed64 | 定长 8 字节。若值常大于25^556^66 则会比 uint64 更高效。 | uint64 |
sfixed32 | 定长 4 字节。 | int32 |
sfixed64 | 定长 8 字节。 | int64 |
bool | bool | |
string | 包含 UTF-8 和 ASCII 编码的字符串,长度不能超过 23^332^22。 | string |
bytes | 可包含任意的字节序列但长度不能超过 23^332^22。 | string |
- [1] 变⻓编码是指:经过protobuf 编码后,原本4字节或8字节的数可能会被变为其他字节数
enum类型
语法格式:
enum PhoneType {MP = 0; // 移动电话TEL = 1; // 固定电话
}
注意点:
- 0值常量必须存在,且为第一个元素
- 同级的enum类型中各个枚举类型中的常量不能重名。
- 单个 .proto 文件下,最外层枚举类型和嵌套枚举类型,不算同级。
- 多个 .proto 文件下,若一个文件引入了其他文件,且每个文件都未声明 package,每个 proto 文件中的枚举类型都在最外层,算同级。
- 多个 .proto 文件下,若一个文件引入了其他文件,且每个文件都声明了 package,不算同级。
例子:
// ---------------------- 情况1:同级枚举类型包含相同枚举值名称--------------------
enum PhoneType {MP = 0; // 移动电话TEL = 1; // 固定电话
}
enum PhoneTypeCopy {MP = 0; // 移动电话 // 编译后报错:MP 已经定义
}// ---------------------- 情况2:不同级枚举类型包含相同枚举值名称-------------------enum PhoneTypeCopy {MP = 0; // 移动电话 // ⽤法正确
}message Phone {
string number = 1; // 电话号码
enum PhoneType {
MP = 0; // 移动电话
TEL = 1; // 固定电话
}// ---------------------- 情况3:多⽂件下都未声明package--------------------
// phone1.proto
import "phone1.proto"
enum PhoneType {MP = 0; // 移动电话 // 编译后报错:MP 已经定义TEL = 1; // 固定电话
}
// phone2.proto
enum PhoneTypeCopy {MP = 0; // 移动电话
}// ---------------------- 情况4:多⽂件下都声明了package--------------------
// phone1.proto
import "phone1.proto"
package phone1;
enum PhoneType {MP = 0; // 移动电话 // ⽤法正确TEL = 1; // 固定电话
}
// phone2.proto
package phone2;
enum PhoneTypeCopy {MP = 0; // 移动电话
}
Any类型
字段还可以声明为 Any 类型,可以理解为泛型类型。使⽤时可以在 Any 中存储任意消息类型。Any 类型的字段也⽤ repeated 来修饰。
Any 类型是 google 已经帮我们定义好的类型,在安装 ProtoBuf 时,其中的 include ⽬录下查找所有google 已经定义好的 .proto ⽂件。
- 使⽤ PackFrom() ⽅法可以将任意消息类型转为 Any 类型。
- 使⽤ UnpackTo() ⽅法可以将 Any 类型转回之前设置的任意消息类型。
- 使⽤ Is() ⽅法可以⽤来判断存放的消息类型是否为 typename T。
oneof类型
如果消息中有很多可选字段, 并且将来同时只有⼀个字段会被设置, 那么就可以使⽤ oneof 加强这个⾏为,也能有节约内存的效果。
oneof other_contact {// repeated string qq = 5; // 不能使用 repeatedstring qq = 5;string wechat = 6;}
注意点:
- 可选字段中的字段编号,不能与⾮可选字段的编号冲突。
- 不能在 oneof 中使⽤ repeated 字段。
- 将来在设置 oneof 字段中值时,如果将 oneof 中的字段设置多个,那么只会保留最后⼀次设置的成员,之前设置的 oneof 成员会⾃动清除。
map 类型
语法⽀持创建⼀个关联映射字段,也就是可以使⽤ map 类型去声明字段类型,格式为:
map<key_type, value_type> map_field = N;
- key_type 是除了 float 和 bytes 类型以外的任意标量类型。 value_type 可以是任意类型。
- map 字段不可以⽤ repeated 修饰
- map 中存⼊的元素是⽆序的
默认值
反序列化消息时,如果被反序列化的⼆进制序列中不包含某个字段,反序列化对象中相应字段时,就会设置为该字段的默认值。不同的类型对应的默认值不同:
- 对于字符串,默认值为空字符串。
- 对于字节,默认值为空字节。
- 对于布尔值,默认值为 false。
- 对于数值类型,默认值为 0。
- 对于枚举,默认值是第⼀个定义的枚举值, 必须为 0。
- 对于消息字段,未设置该字段。它的取值是依赖于语⾔。
- 对于设置了 repeated 的字段的默认值是空的( 通常是相应语⾔的⼀个空列表 )。
- 对于 消息字段 、 oneof字段 和 any字段 ,C++ 和 Java 语⾔中都有 has_ ⽅法来检测当前字段是否被设置。
字段编号规则
编号范围 | 说明 |
---|---|
1 ~ 15 | 需要一个字节进行编码,建议用于频繁出现的字段 |
16 ~ 2047 | 需要两个字节进行编码 |
1 ~ 536,870,911 (2^29 - 1) | 总的有效范围 |
19000 ~ 19999 | 不可用(Protobuf 协议预留) |
proto3语法
字段规则
- singular :消息中可以包含该字段零次或⼀次(不超过⼀次)。 proto3 语法中,字段默认使⽤该规则。
- repeated :消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。可以理解为定义了⼀个数组。
消息类型的定义与使用
定义
在单个 .proto ⽂件中可以定义多个消息体,且⽀持定义嵌套类型的消息(任意多层)。每个消息体中的字段编号可以重复。
// -------------------------- 嵌套写法 -------------------------
syntax = "proto3";
package contacts;
message PeopleInfo {string name = 1; int32 age = 2; message Phone {string number = 1;}
}
// -------------------------- ⾮嵌套写法 -------------------------
syntax = "proto3";
package contacts;
message Phone {string number = 1;
}
message PeopleInfo {string name = 1; int32 age = 2;
}
使用
- 消息类型可作为字段类型使⽤
syntax = "proto3";
package contacts;
// 联系⼈
message PeopleInfo {string name = 1; int32 age = 2; message Phone {string number = 1; }repeated Phone phone = 3;
}
- 可导⼊其他 .proto ⽂件的消息并使⽤
例如 Phone 消息定义在 phone.proto ⽂件中:
//phone.proto
syntax = "proto3";
package phone;
message Phone {
string number = 1;
}
假设同级下有一个.proto文件想要使用我这个message
syntax = "proto3";
package contacts;
import "phone.proto"; // 使⽤ import 将 phone.proto ⽂件导⼊进来 !!!
message PeopleInfo {string name = 1; int32 age = 2; // 引⼊的⽂件声明了package,使⽤消息时,需要⽤ ‘命名空间.消息类型’ 格式 repeated phone.Phone phone = 3;
}
protoc指令
protoc -h //查看所有的选项
编译 contacts.proto 生成 C++ 代码
# 生成到当前目录
protoc --cpp_out=【当前目录】 【proto文件名】# 生成到指定目录
protoc --cpp_out=【目标目录】 【proto文件名】# 指定导入路径
protoc -I=【proto文件所在目录】 --cpp_out=【代码输出目录】 【proto文件完整路径】
#【proto文件完整路径】:这个就是相对于你指定【proto文件所在目录】的相对路径即可
对序列化的数据通过–decode选项进行解码
//因为我们protobuf进行序列化后存储的是字节编码 肉眼看不懂
protoc --decode=【消息类型全名】(有package也要加上package) 【proto文件】 < 【二进制消息文件】
protoc --decode=contacts.Contacts contacts.proto < contacts.bin//hexdump:是Linux下的⼀个⼆进制⽂件查看⼯具,它可以将⼆进制⽂件转换为ASCII、⼋进制、
//⼗进制、⼗六进制格式进⾏查看。
// -C: 表⽰每个字节显⽰为16进制和相应的ASCII字符
消息更新规则
如果现有的消息类型已经不再满⾜我们的需求,例如需要扩展⼀个字段,在不破坏任何现有代码的情况下更新消息类型⾮常简单。遵循如下规则即可:
- 禁⽌修改任何已有字段的字段编号。
- 若是移除⽼字段,要保证不再使⽤移除字段的字段编号。正确的做法是保留字段编号
(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 类型是不安全的
对于oneof的解释:
- 将⼀个单独的值更改为新 oneof 类型成员之⼀是安全和⼆进制兼容的
含义:可以将现有的单个字段移动到一个新创建的 oneof 中,这是安全的。
// 原始版本
message User {string name = 1;int32 age = 2;string email = 3;
}// 安全更新:将 email 字段移到新的 oneof 中
message User {string name = 1;int32 age = 2;oneof contact_method {string email = 3; // 原来就是字段3,现在放入新的oneofstring phone = 4; // 新增字段}
}
- 若确定没有代码⼀次性设置多个值那么将多个字段移⼊⼀个新 oneof 类型也是可⾏的
含义: 可以将多个现有的字段一起移动到一个新创建的 oneof 中,但前提是这些字段不会同时被设置。
// 原始版本
message Payment {string credit_card = 1;string paypal_id = 2;string bank_transfer = 3;
}// 可能安全的更新(如果业务上这些支付方式不会同时使用)
message Payment {oneof payment_method {string credit_card = 1; // 从普通字段移到oneofstring paypal_id = 2; // 从普通字段移到oneof string bank_transfer = 3; // 从普通字段移到oneof}
}
- 将任何字段移⼊已存在的 oneof 类型是不安全的
含义: 不能将新字段添加到已经存在的 oneof 中。
// 原始版本
message User {oneof identity {string email = 1;string phone = 2;}string wechat_id = 3; // 普通字段
}// 不安全的更新:将现有字段添加到已存在的oneof中
message User {oneof identity {string email = 1;string phone = 2;string wechat_id = 3; // 危险!将普通字段移入已有oneof}
}
保留字段 reserved
如果通过 删除 或 注释掉 字段来更新消息类型,未来的⽤⼾在添加新字段时,有可能会使⽤以前已经存在,但已经被删除或注释掉的字段编号。将来使⽤该 .proto 的旧版本时的程序会引发很多问题:数据损坏、隐私错误等等。
syntax = "proto3";package example;// ==================== 原始版本 (更新前) ====================
message UserInfo_Original {string name = 1;string password = 2; // 用户密码字段string email = 3;int32 user_type = 4; // 1=普通用户, 2=管理员string phone = 5;
}// ==================== 危险更新做法 ====================
message UserInfo_Danger {string name = 1;// string password = 2; // 危险:直接删除敏感字段string email = 3;int32 user_type = 4;string phone = 5;string nickname = 2; // 致命错误:重用已删除字段的编号!// 旧数据中的密码会被错误解析为昵称
}// ==================== 正确更新做法 ====================
message UserInfo_Correct {string name = 1;// 语法: reserved 2, 10, 11, 100 to 200; reserved 2; // 正确:保留已删除字段的编号reserved "password"; // 正确:保留已删除字段的名称string email = 3;int32 user_type = 4;string phone = 5;string nickname = 6; // 正确:使用新的字段编号
}// ==================== 数据损坏演示 ====================
// 假设旧版本数据:
// name = "Alice"
// password = "secret123" (字段2)
// email = "alice@example.com"
// user_type = 1
//
// 使用 UserInfo_Danger 解析时:
// name = "Alice" ✓
// nickname = "secret123" ✗ (密码泄露!)
// email = "alice@example.com" ✓
// user_type = 1 ✓
//
// 使用 UserInfo_Correct 解析时:
// name = "Alice" ✓
// email = "alice@example.com" ✓
// user_type = 1 ✓
// nickname = "" ✓ (安全,不会泄露密码)
获取未知字段
就比如当我们上面的例子当我们用reserved保留字段正确跟新后,我们去反序列化的时候就不会解析到原来的字段password编号2,该字段放在了我们的未知字段中。
如何获取未知字段
MessageLite 类介绍
- MessageLite 从名字看是轻量级的 message,仅仅提供序列化、反序列化功能。
- 类定义在 google 提供的 message_lite.h 中。
Message 类
- 我们⾃定义的message类,都是继承⾃Message。
- Message 最重要的两个接⼝ GetDescriptor/GetReflection,可以获取该类型对应的Descriptor对象指针 和 Reflection 对象指针。
- 类定义在 google 提供的 message.h 中。
//google::protobuf::Message 部分代码展⽰
const Descriptor* GetDescriptor() const;const Reflection* GetReflection() const;
Descriptor 类
- Descriptor:是对message类型定义的描述,包括message的名字、所有字段的描述、原始的proto⽂件内容等。
- 类定义在 google 提供的 descriptor.h 中。
Reflection 类
- Reflection接⼝类,主要提供了动态读写消息字段的接⼝,对消息对象的⾃动读写主要通过该类完成。
- 提供⽅法来动态访问/修改message中的字段,对每种类型,Reflection都提供了⼀个单独的接⼝⽤于读写字段对应的值。
- 针对所有不同的field类型 FieldDescriptor::TYPE_* ,需要使⽤不同的 Get*()/Set*()/Add*() 接⼝;
- repeated类型需要使⽤ GetRepeated*()/SetRepeated*() 接⼝,不可以和⾮repeated类型接⼝混⽤;
- message对象只可以被由它⾃⾝的 reflection(message.GetReflection()) 来操作;
- 类中还包含了访问/修改未知字段的⽅法。
- 类定义在 google 提供的 message.h 中。
UnknownFieldSet 类
- UnknownFieldSet 包含在分析消息时遇到但未由其类型定义的所有字段。
- 若要将 UnknownFieldSet 附加到任何消息,请调⽤Reflection::GetUnknownFields()。
- 类定义在 unknown_field_set.h 中。
UnknownField 类介绍
- 表⽰未知字段集中的⼀个字段。
- 类定义在 unknown_field_set.h 中。
option选项
.proto ⽂件中可以声明许多选项,使⽤ option 标注。选项能影响 proto 编译器的某些处理⽅式。
选项分类
//部分选项
syntax = "proto2"; // descriptor.proto 使⽤ proto2 语法版本
message FileOptions { ... } // ⽂件选项 定义在 FileOptions 消息中
message MessageOptions { ... } // 消息类型选项 定义在 MessageOptions 消息中
message FieldOptions { ... } // 消息字段选项 定义在 FieldOptions 消息中
message OneofOptions { ... } // oneof字段选项 定义在 OneofOptions 消息中message EnumOptions { ... } // 枚举类型选项 定义在 EnumOptions 消息中
message EnumValueOptions { .. } // 枚举值选项 定义在 EnumValueOptions 消息中
message ServiceOptions { ... } // 服务选项 定义在 ServiceOptions 消息中
message MethodOptions { ... } // 服务⽅法选项 定义在 MethodOptions 消息中
常用选项列举
- optimize_for : 该选项为⽂件选项,可以设置 protoc 编译器的优化级别,分别为 SPEED 、CODE_SIZE 、 LITE_RUNTIME 。受该选项影响,设置不同的优化级别,编译 .proto ⽂件后⽣成的代码内容不同。
- SPEED : protoc 编译器将⽣成的代码是⾼度优化的,代码运⾏效率⾼,但是由此⽣成的代码编译后会占⽤更多的空间。 是默认选项。
- CODE_SIZE : proto 编译器将⽣成最少的类,会占⽤更少的空间,是依赖基于反射的代码来实现序列化、反序列化和各种其他操作。但和 SPEED 恰恰相反,它的代码运⾏效率较低。这种⽅式适合⽤在包含⼤量的.proto⽂件,但并不盲⽬追求速度的应⽤中。
- LITE_RUNTIME : ⽣成的代码执⾏效率⾼,同时⽣成代码编译后的所占⽤的空间也是⾮常少。这是以牺牲Protocol Buffer提供的反射功能为代价的,仅仅提供 encoding+序列化 功能,所以我们在链接 BP 库时仅需链接libprotobuf-lite,⽽⾮libprotobuf。这种模式通常⽤于资源
有限的平台,例如移动⼿机平台中。
option optimize_for = LITE_RUNTIME;
- allow_alias: 允许将相同的常量值分配给不同的枚举常量,⽤来定义别名。该选项为枚举选项。
举个例⼦:
enum PhoneType {option allow_alias = true;MP = 0;TEL = 1;LANDLINE = 1; // 若不加 option allow_alias = true; 这⼀⾏会编译报错
}
网络版通讯录实战
环境搭建
Httplib 库:cpp-httplib 是个开源的库,是一个c++封装的http库,使用这个库可以在linux、windows平台下完成http客户端、http服务端的搭建。使用起来非常方便,只需要包含头文件httplib.h 即可。编译程序时,需要带上 -lpthread 选项。
下载链接:源代码仓库
大致流程
添加联系人实现
AddPeople.proto文件的定义(客户端服务器一样)
syntax ="proto3";
package AddPeople;
import "google/protobuf/any.proto"; // 添加 Any 类型的导入message Address
{string home_address=1;string work_address=2;
}
message AddPeopleRequest
{string name=1;//姓名int32 age=2;//年龄message Phone//电话{string phone=1;enum PhoneType {MP = 0; // 移动电话TEL = 1; // 固定电话}PhoneType phone_type=2;}repeated Phone phone=3;//电话google.protobuf.Any address=4;//用Any类型保存我们的地址信息oneof other_contact {// repeated string qq = 5; // 不能使用 repeatedstring qq = 5;string wechat = 6;}map<string,string> remark=7;//备注信息string uid = 8;
}
message AddPeopleResponse
{bool success=1;//是否成功string error_desc=2;//错误描述string uid=3;//对于每个添加的联系人服务器返回一个uid}
自定义异常类ContactException.h
#pragma once
#include <string>
class ContactException
{
private:std::string _message;public:ContactException(std::string message) : _message(message) {}std::string what() const { return _message; }
};
客户端代码实现
contacts.hpp
#ifndef CONTACT_H
#define CONTACT_H
#include "AddPeople.pb.h"
#include "../httplib.h"
#define IP "127.0.0.1"
#define PORT 8086
class Contact
{private:httplib::Client _client;private:void BuildHttpBody(AddPeople::AddPeopleRequest &req){std::cout << "请输入联系人姓名: ";std::string name;getline(std::cin, name);req.set_name(name);std::cout << "请输入联系人年龄: ";int age;std::cin >> age;req.set_age(age);std::cin.ignore(256, '\n');for (int i = 1;; i++){std::cout << "请输入联系人电话" << i << "(只输入回车完成电话新增): ";std::string number;getline(std::cin, number);if (number.empty()){break;}// 返回指向一段指向phone字段的空间让我们进行操作AddPeople::AddPeopleRequest_Phone *phone = req.add_phone();phone->set_phone(number);std::cout << "选择此电话类型 (1、移动电话 2、固定电话) : ";int type;std::cin >> type;std::cin.ignore(256, '\n');switch (type){case 1:phone->set_phone_type(AddPeople::AddPeopleRequest_Phone_PhoneType::AddPeopleRequest_Phone_PhoneType_MP);break;case 2:phone->set_phone_type(AddPeople::AddPeopleRequest_Phone_PhoneType::AddPeopleRequest_Phone_PhoneType_TEL);break;default:std::cout << "----非法选择,使用默认值!" << std::endl;break;}}AddPeople::Address address;std::cout << "请输入联系人家庭地址: ";std::string home_address;getline(std::cin, home_address);address.set_home_address(home_address);std::cout << "请输入联系人单位地址: ";std::string unit_address;getline(std::cin, unit_address);address.set_work_address(unit_address);// 返回一段可操作空间google::protobuf::Any *data = req.mutable_address();// 设置我们的any字段data->PackFrom(address);// 对其他联系方式oneof字段进行操作std::cout << "请选择要添加的其他联系方式(1、qq 2、微信号):";int other_contact;std::cin >> other_contact;std::cin.ignore(256, '\n');if (1 == other_contact){std::cout << "请输入联系人qq号: ";std::string qq;getline(std::cin, qq);req.set_qq(qq);}else if (2 == other_contact){std::cout << "请输入联系人微信号: ";std::string wechat;getline(std::cin, wechat);req.set_wechat(wechat);}else{std::cout << "选择有误,未成功设置其他联系方式!" << std::endl;}for (int i = 0;; i++){std::cout << "请输入备注" << i + 1 << "标题(只输入回车完成备注新增):";std::string remark_key;std::getline(std::cin, remark_key);if (remark_key.empty()){break;}std::cout << "请输入备注" << i + 1 << "内容: ";std::string remark_value;getline(std::cin, remark_value);// 返回操作我们map的指针req.mutable_remark()->insert({remark_key, remark_value});}}public:Contact() : _client(IP, PORT){}void AddOnePeople(){// http的完整请求httplib::Request req;// 构建http请求的正文AddPeople::AddPeopleRequest request;// 从表中输入中拿到我们的联系人信息把我们的post中的body结构体填充完毕BuildHttpBody(request);// 进行序列化到http请求正文中std::string body_str;if (!request.SerializeToString(&body_str)){// 抛出异常throw ContactException("请求body序列化失败");}// 下面进行发送// res 里面包括我们的http的响应auto res = _client.Post("/contacts/add", body_str, "application/x-protobuf");if (!res){std::string err_desc;err_desc.append("/contacts/add 链接错误!错误信息:").append(httplib::to_string(res.error()));throw ContactException(err_desc);}// 进行反序列化我们的应答的bodyAddPeople::AddPeopleResponse response;bool parse = response.ParseFromString(res->body);if (!parse && res->status != 200){// 反序列化失败 并且状态码出错std::string err_desc;err_desc.append("post '/contacts/add/' 失败:").append(std::to_string(res->status)).append("(").append(res->reason).append(")");throw ContactException(err_desc);}else if (res->status != 200){// 反序列化成功 但是状态码不对// 处理服务异常std::string err_desc;err_desc.append("post '/contacts/add/' 失败 ").append(std::to_string(res->status)).append("(").append(res->reason).append(") 错误原因:").append(response.error_desc());throw ContactException(err_desc);}else if (!response.success()){// 处理结果异常std::string err_desc;err_desc.append("post '/contacts/add/' 结果异常:").append("异常原因:").append(response.error_desc());throw ContactException(err_desc);}std::cout << "---> 新增联系人成功,联系人ID:" << response.uid() << std::endl;}void DelOnePeople(){}void FindOnePeople(){}void FindAllPeople(){}
};
#endif
client.cc
#include "../httplib.h"
#include "AddPeople.pb.h"
#include "ContactException.h"
#include "contacts.hpp"
#include <iostream>
#include <string>
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;
}
typedef enum
{QUIT,ADDPEOPLE,DEL,FIND_ALL,FIND_ONE
} OPTION;int main()
{Contact contact;try{int choose;do{menu();std::cout << "---> 请选择:";std::cin >> choose;std::cin.ignore(256, '\n');switch (choose){case OPTION::ADDPEOPLE:contact.AddOnePeople();break;case OPTION::DEL:break;case OPTION::FIND_ALL:break;case OPTION::FIND_ONE:break;case OPTION::QUIT:std::cout << "退出程序" << std::endl;break;default:std::cout << "选择错误请重新输入" << std::endl;break;}} while (choose != 0);}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;}return 0;
}
服务端代码实现
Contacts.proto
syntax ="proto3";
package ContactsServe;
import "AddPeople.proto";
//用于本地存储
message contactsserve
{repeated AddPeople.AddPeopleRequest people=1;
}
serve.hpp
#pragma once#include "../httplib.h"
#include "Contacts.pb.h"
#include "ContactException.h"
#include <fstream>
#include <random>
#include <sstream>
#include <unordered_map>class Serve
{private:httplib::Server _serve;ContactsServe::contactsserve _contacts;std::unordered_map<std::string, AddPeople::AddPeopleRequest> _uid_people;private:// 打印联系人信息的私有函数void PrintContactInfo(const AddPeople::AddPeopleRequest &people, const std::string &title = "联系人信息"){std::cout << "\n=== " << title << " ===" << std::endl;std::cout << "UID: " << people.uid() << std::endl;std::cout << "姓名: " << people.name() << std::endl;std::cout << "年龄: " << people.age() << std::endl;// 打印电话号码for (int i = 0; i < people.phone_size(); i++){const auto &phone = people.phone(i);std::string phone_type_str = (phone.phone_type() == AddPeople::AddPeopleRequest_Phone_PhoneType_MP) ? "手机" : "固话";std::cout << "电话" << (i + 1) << ": " << phone.phone()<< " (类型: " << phone_type_str << ")" << std::endl;}// 打印地址信息if (people.has_address()){AddPeople::Address address;if (people.address().UnpackTo(&address)){std::cout << "家庭地址: " << address.home_address() << std::endl;std::cout << "工作地址: " << address.work_address() << std::endl;}}// 打印其他联系方式if (people.has_qq()){std::cout << "QQ: " << people.qq() << std::endl;}if (people.has_wechat()){std::cout << "微信: " << people.wechat() << std::endl;}// 打印备注信息for (const auto &remark : people.remark()){std::cout << "备注[" << remark.first << "]: " << remark.second << std::endl;}std::cout << "==========================\n"<< std::endl;}void OnAddpeople(const httplib::Request &requ, httplib::Response &res){try{// 生成唯一IDstd::string uid = generate_hex(8);// 对请求进行反序列化AddPeople::AddPeopleRequest people;bool parse = people.ParseFromString(requ.body);if (!parse){throw ContactException("反序列化请求body失败");}// 设置服务器分配的 UIDpeople.set_uid(uid);// 打印联系人信息PrintContactInfo(people, "新添加的联系人信息");// 反序列化成功后加入我们的通讯录AddPeople::AddPeopleRequest *onepeople = _contacts.add_people();onepeople->CopyFrom(people);// 更新内存映射_uid_people[uid] = people;// 保存到文件std::ofstream out("./contacts.bin", std::ios::binary);if (!_contacts.SerializeToOstream(&out)){throw ContactException("保存通讯录到文件失败");}out.close();// 返回成功响应AddPeople::AddPeopleResponse response;response.set_success(true);response.set_uid(uid);std::string response_str;response.SerializeToString(&response_str);res.set_content(response_str, "application/x-protobuf");res.status = 200;}catch (const ContactException &e){res.status = 400;res.set_content(e.what(), "text/plain");}catch (const std::exception &e){res.status = 500;res.set_content(e.what(), "text/plain");}}// 生成随机字符unsigned int random_char(){std::random_device rd;std::mt19937 gen(rd());std::uniform_int_distribution<> dis(0, 255);return dis(gen);}// 生成 UUIDstd::string generate_hex(const unsigned int len){std::stringstream ss;for (auto i = 0; i < len; i++){const auto rc = random_char();std::stringstream hexstream;hexstream << std::hex << rc;auto hex = hexstream.str();ss << (hex.length() < 2 ? '0' + hex : hex);}return ss.str();}public:Serve(){// 把磁盘文件加载我们的内存_contactsstd::ifstream in("./contacts.bin", std::ios::binary);if (in.is_open() == false){std::cout << "通讯录文件不存在,将创建新文件" << std::endl;return;}if (!_contacts.ParseFromIstream(&in)){std::cerr << "加载本地磁盘通讯录失败,将使用空通讯录" << std::endl;}else{std::cout << "成功加载 " << _contacts.people_size() << " 个联系人" << std::endl;// 进行我们的_uid_people构建for (int i = 0; i < _contacts.people_size(); i++){const AddPeople::AddPeopleRequest &person = _contacts.people(i);// 打印已加载的联系人信息PrintContactInfo(person, "已加载的联系人 " + std::to_string(i + 1));_uid_people[person.uid()] = person;}}in.close();}public:void HandlerAddOnePeople(){_serve.Post("/contacts/add", [this](const httplib::Request &requ, httplib::Response &res){ this->OnAddpeople(requ, res); });}void Start(){HandlerAddOnePeople(); // 注册路由 - 这行必须添加!std::cout << "服务器启动在 0.0.0.0:8086" << std::endl;_serve.listen("0.0.0.0", 8086);}
};
main.cc
#include "serve.hpp"
#include "ContactException.h"
int main()
{try{Serve serve;serve.Start();}catch (const ContactException &e){std::cerr << e.what() << '\n';}catch (const std::exception &e){std::cerr << e.what() << '\n';}return 0;
}
下面其他具体功能就没有实现了 大家感兴趣可以自己搞一下~