protoBuf的基础使用可参看ProtoBuf基础使用
本篇博客依旧以通讯录为例展开讲解,语法为proto3
当前通讯录属性如下:
message PeopleInfo{
string name = 1;
int32 age = 2;
}
经过学习,实现通讯录如下功能:
- 新增联系人属性,共包括:姓名,年龄,电话信息,地址,其他联系方式,备注
- 将通讯录序列化后写入文件
- 从文件中将通讯录解析出来,并打印
文章目录
字段规则
消息字段可以用如下两种规则修饰:
- sigular:消息中可以包含该字段零次或一次(至多一次)。proto3语法中,字段默认使用该规则
- repeated:消息中可以包含该字段任意次(包括零次),其中重复值的顺序会被保留。可以理解为定义了一个数组
使用repeated
新增phone_numbers
,表示一个联系人可以有多个号码,可以将其设置为repeated
syntax = "proto3";
package contacts;
message PeopleInfo{
string name = 1;
int32 age = 2;
repeated string phone_numbers = 3;
}
常用API
- 获取元素个数
inline int PeopleInfo::phone_size()
//示例:
PeopleInfo people;
int n = people.phone_size();
- 通过下标访问
inline const std::string& PeopleInfo::phone(int index)
//示例:
PeopleInfo people;
for(int i = 0; i < people.phone_size(); ++i)
auto phone = people.phone(i);
- 添加元素
//返回开辟好的空间
inline std::string* PeopleInfo::add_phone_numbers()
//示例:
PeopleInfo people;
std::string *name = people->add_phone_numbers();
name1 = &string("你好");
消息类型的定义和使用
定义
在单个 .proto 文件中可以定义多个消息体,且支持定义嵌套类型的消息(任意多层)。每个消息体的字段编号可以重复
更新 contacts.proto ,将 phone_number 设置成一个消息,包含电话号码
syntax = "proto3";
package contacts;
//嵌套写法
message PeopleInfo{
string name = 1;
int32 age = 2;
message Phone{
string number = 1;
}
}
//非嵌套写法
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;//使用
}
//非嵌套写法
message Phone{//定义
string number = 1;
}
message PeopleInfo{
string name = 1;
int32 age = 2;
repeated Phone phone = 3;//使用
}
也可以单独编写 .proto 文件,然后导入
将 Phone 消息定义在 phone.proto
文件中
phone.proto
syntax = "proto3";
package phone;
message Phone{
string number = 1;
}
contact.proto
syntax = "proto3";
package contacts;
import "phone.proto //导入
message PeopleInfo{
string name = 1;
int32 age = 2;
repeated phone.Phone phone = 3;//需要指明package
}
更新通讯录
syntax = "proto3";
package contacts;
//嵌套写法
message PeopleInfo{
string name = 1; //姓名
int32 age = 2; //年龄
message Phone{
string number = 1;
}
repeated Phone phone = 3; //电话
}
message Contacts{
repeated PeopleInfo peoples = 1; //多个联系人
}
总结:
- 每个字段都有一个
clear_
方法,可以将字段重新设置回 empty 状态- 每个字段都有设置和获取方法,获取字段的方法名称和小写字段名称完全相同。但如果是消息类型的字段,其设置方法为
mutable_
方法,返回值为消息类型的指针,这类方法会为我们开辟好空间,可以直接对这块空间的内容进行修改- 对于 repeated 修饰的字段,也就是数组类型,protoBuf 为我们提供了
add_
方法来新增一个值,并且提供了_size
方法来判断数组存放元素的个数
读写通讯录
write.cc
:用于获取联系人信息,序列化,然后写入文件
#include<iostream>
#include<fstream>
#include"contacts.pb.h"
using namespace std;
using namespace contacts;
/**新增联系⼈*/
voidAddPeopleInfo(PeopleInfo *people_info_ptr)
{
cout <<"-------------新增联系⼈-------------"<< endl;
cout <<"请输⼊联系⼈姓名: ";string name;
getline(cin, name);
people_info_ptr->set_name(name);
cout <<"请输⼊联系⼈年龄: ";intage;
cin >> age;
people_info_ptr->set_age(age);
cin.ignore(256,'\n');
for(inti =1; ; i++) {
cout <<"请输⼊联系⼈电话"<< i <<"(只输⼊回⻋完成电话新增): ";
string number;
getline(cin, number);
if(number.empty())
break;
PeopleInfo_Phone* phone = people_info_ptr->add_phone();
phone->set_number(number);
}
cout <<"-----------添加联系⼈成功-----------"<< endl;
}
intmain(intargc,char*argv[])
{
// GOOGLE_PROTOBUF_VERIFY_VERSION宏:
//验证没有意外链接到与编译的头⽂件不兼容的库版本。
//如果检测到版本不匹配,程序将中⽌。注意,每个.pb.cc⽂件在启动时都会⾃动调⽤此宏。
//在使⽤C++ Protocol Buffer库之前执⾏此宏是⼀种很好的做法,但不是绝对必要的。
GOOGLE_PROTOBUF_VERIFY_VERSION;
if(argc !=2){
cerr <<"Usage: "<< argv[0] <<" CONTACTS_FILE"<< endl;
return-1;
}
Contacts contacts;
//先读取已存在的contacts
fstreaminput(argv[1], ios::in | ios::binary);
if(!input)
cout << argv[1] <<": 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
fstreamoutput(argv[1], 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();
//在程序结束时调⽤ShutdownProtobufLibrary(),
//为了删除Protocol Buffer库分配的所有全局对象。对于⼤多数程序来说这是不必要的,
//因为该过程⽆论如何都要退出,并且操作系统将负责回收其所有内存。
//但是,如果你使⽤了内存泄漏检查程序,该程序需要释放每个最后对象,
//或者你正在编写可以由单个进程多次加载和卸载的库,
//那么你可能希望强制使⽤Protocol Buffers来清理所有内容。
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
makefile
write:write.cc contacts.pb.cc
g++ -o $@ $^ -std=c++11 -lprotobuf
.PHONY:clean
clean:
rm -f write
运行结果如下:
hyb@139-159-150-152:~/project/protobuf/contacts$ make
g++ -o write write.cc contacts.pb.cc -std=c++11 -lprotobufhyb
@139-159-150-152:~/project/protobuf/contacts$ ./write contacts.bin
contacts.bin: File not found. Creating a new file.
-------------新增联系⼈-------------
请输⼊联系⼈姓名:张三
请输⼊联系⼈年龄: 20
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 13111111111
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增): 15111111111
请输⼊联系⼈电话3(只输⼊回⻋完成电话新增):
-----------添加联系⼈成功-----------
查看二进制文件
hexdump
:是Linux下的⼀个⼆进制⽂件查看⼯具,它可以将⼆进制⽂件转换为ASCII、⼋进制、
⼗进制、⼗六进制格式进⾏查看。
-C:表⽰每个字节显⽰为16进制和相应的ASCII字符
hyb@139-159-150-152:~/project/protobuf/contacts$hexdump -C contacts.bin
00000000 0a 28 0a 06 e5 bc a0 e4 b8 89 10 14 1a 0d 0a 0b |.(..............|
00000010 31 33 31 31 31 31 31 31 31 31 31 1a 0d 0a 0b 31 |13111111111....1|
00000020 35 31 31 31 31 31 31 31 31 31 |5111111111|0000002a
read.cc
:用于从文件中读取数据,反序列化,然后打印
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace contacts;
/**打印联系⼈列表*/
voidPrintfContacts(constContacts& contacts)
{
for(inti =0; i < contacts.contacts_size(); ++i)
{
const PeopleInfo& people = contacts.contacts(i);
cout <<"------------联系⼈"<< i+1<<"------------"<< endl;cout <<"姓名:"<< people.name() << endl;
cout <<"年龄:"<< people.age() << endl;intj =1;
for(const PeopleInfo_Phone& phone : people.phone())
cout <<"电话"<< j++ <<": "<< phone.number() << endl;
}
}
int main(intargc,char* argv[])
{
GOOGLE_PROTOBUF_VERIFY_VERSION;
if(argc !=2) {
cerr <<"Usage: "<< argv[0] <<"CONTACTS_FILE"<< endl;
return-1;
}
//以⼆进制⽅式读取contacts
Contacts contacts;
fstreaminput(argv[1], ios::in | ios::binary);
if(!contacts.ParseFromIstream(&input)) {
cerr <<"Failed to parse contacts."<< endl;
input.close();
return-1;
}
//打印contacts
PrintfContacts(contacts);
input.close();
google::protobuf::ShutdownProtobufLibrary();
return0;
}
更新makefile
all:write read
write:write.cc contacts.pb.cc
g++ -o $@ $^ -std=c++11 -lprotobuf
read:read.cc contacts.pb.cc
g++ -o $@ $^ -std=c++11 -lprotobuf
.PHONY:clean
clean:
rm -f write read
运行结果:
hyb@139-159-150-152:~/project/protobuf/contacts$ make
g++ -o read read.cc contacts.pb.cc -std=c++11 -lprotobufhyb
@139-159-150-152:~/project/protobuf/contacts$ ./read contacts.bin
------------联系⼈1------------
姓名:张三
年龄:20
电话1:13111111111
电话2:15111111111
enum类型
定义
语法支持我们定义枚举类型并使用。在 .proto 文件中枚举类型的书写规范为:
枚举类型名称:
使用驼峰命名法,首字母大写。例如:MyEnum
常量值名称:
全大写字母,多个字母之间用 _ 连接。例如:ENUM_CONST = 0;
我们定义一个电话类型的枚举类型:
enum PhoneType{
MP = 0; //移动电话
TEL = 1; //固定电话
}
枚举类型的定义规则如下:
- 0值常量必须存在,且要作为一个元素。这是为了和 proto2 语义兼容:第一个元素作为默认值,且值为0
- 枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)
- 枚举的常量值在 32 位整数范围,负值无效不建议使用
定义时注意
将两个 “具有相同枚举值名称” 的枚举类型放在单个 .proto 文件下时,编译会报错
- 同级(同层)的枚举类型,各个枚举类型中的常量不能重名
- 单个 .proto 文件下,最外层枚举类型和嵌套枚举类型,不算同级
- 多个 .proto 文件下,若一个文件引入其他文件,且每个文件都未声明 package,那么每个 .proto 文件最外层的枚举类型都算同级
- 多个 .proto 文件,若都有声明 package,则不算同级
更新通讯录
为电话号码添加电话类型(枚举类型)
syntax = "proto3";
package contacts;
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; //电话
}
message Contacts{
repeated PeopleInfo peoples = 1; //多个联系人
}
编译后形成的 .h文件
//新⽣成的PeopleInfo_Phone_PhoneType枚举类
enumPeopleInfo_Phone_PhoneType:int{
PeopleInfo_Phone_PhoneType_MP =0,
PeopleInfo_Phone_PhoneType_TEL =1,
PeopleInfo_Phone_PhoneType_PeopleInfo_Phone_PhoneType_INT_MIN_SENTINEL_DO_NOT_USE_ = std::numeric_limits<int32_t>::min(),
PeopleInfo_Phone_PhoneType_PeopleInfo_Phone_PhoneType_INT_MAX_SENTINEL_DO_NOT_USE_ = std::numeric_limits<int32_t>::max()
};
//更新的PeopleInfo_Phone类
public:
typedef PeopleInfo_Phone_PhoneType PhoneType;
static inline bool PhoneType_IsValid(intvalue) {
return PeopleInfo_Phone_PhoneType_IsValid(value);
}
template<typenameT>
static inline const std::string& PhoneType_Name(T enum_t_value){...}
static inline bool PhoneType_Parse(::PROTOBUF_NAMESPACE_ID::ConstStringParam name, PhoneType* value) {...}
// .contacts.PeopleInfo.Phone.PhoneType type = 2;
voidclear_type();
::contacts::PeopleInfo_Phone_PhoneTypetype()const;
voidset_type(::contacts::PeopleInfo_Phone_PhoneType value);
};
接口:
- 枚举类型编译生成的代码中会含有与之对应的枚举类型,校验枚举值是否有效的方法
_IsVaild
、以及获取枚举值名称的方法_Name
- 对于使用了枚举类型的字段,包含设置和获取方法,以及清空字段的方法
clear_
write.cc
在添加电话中新增关于电话类型的选择
//输入电话
for(int i = 0; ; ++i)
{
//电话号码
std::string phone;
std::cout << "请输入联系人电话(仅输入回车表示结束): ";
getline(std::cin, phone);
if(phone.empty())
break;//仅输入回车
contacts2::PeopleInfo_Phone *people_phone = people->add_phone();
people_phone->set_phone(phone);
//电话类型
std::cout << "请输入电话类型(1. 移动电话 2. 固定电话): ";
int type = 0;
std::cin >> type;
std::cin.ignore(256, '\n');
//判断电话类型
switch(type)
{
case 1:
people_phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
break;
case 2:
people_phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);
break;
default:
std::cout << "输入有误!" << std::endl;
break;
}
}
read.cc
新增打印电话类型
//电话
for(int j = 0; j < people.phone_size(); ++j)
{
const contacts2::PeopleInfo_Phone &phone = people.phone(j);
std::cout << j + 1 << "电话: " << phone.phone()
<< " (" << phone.PhoneType_Name(phone.type()) << ")" << std::endl;
}
Any 类型
字段还可以声明为 Any 类型,可以理解为泛型类型。使用时可以在 Any 中存储任意消息类型。Any 类型的字段也可以使用 repeated 修饰
Any 是 google 定义的类型,使用时需要包含相应 .proto文件
更新通讯录
添加地址信息,在PeopleInfo 中使用 Any 接收
syntax = "proto3";
package contacts;
import "goole/protobuf/any.proto"
//地址
message Address
{
string host_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;
}
message Contacts{
repeated PeopleInfo peoples = 1; //多个联系人
}
接口:
- 对于Any 类型字段,获取方法的方法名称同小写字段名称,设置方法使用
mutable_
方法,返回值为 Any 类型的指针,这类方法会返回开辟好的空间,可以直接对该控件内容进行修改 - Any 类型可接收任意类型,互相转化的方法在
any.pb.h
class PROTOBUF_EXPORT Any final : public::PROTOBUF_NAMESPACE_ID::Message {
boolPackFrom(const::PROTOBUF_NAMESPACE_ID::Message& message) {
...
}
boolUnpackTo(::PROTOBUF_NAMESPACE_ID::Message* message)const{
...
}
template<typename T>boolIs()const{
return_impl_._any_metadata_.Is<T>();
}
};
解释:
使⽤PackFrom()⽅法可以将任意消息类型转为Any类型。
使⽤UnpackTo()⽅法可以将Any类型转回之前设置的任意消息类型。
使⽤Is()⽅法可以⽤来判断存放的消息类型是否为typename<T>。
write.cc
新增添加地址
//家庭地址
contacts2::Address address;
std::cout << "请输入家庭地址:";
std::string host_address;
getline(std::cin, host_address);
if(!host_address.empty())
address.set_host_address(host_address);
std::cout << "请输入单位地址:";
std::string unit_address;
getline(std::cin, unit_address);
if(!unit_address.empty())
address.set_unit_address(unit_address);
//使用泛型Any接收
google::protobuf::Any *data = people->mutable_data();
data->PackFrom(address);
read.cc
新增打印地址
//地址
//泛型有数据,并且类型匹配
if(people.has_data() && people.data().Is<contacts2::Address>())
{
contacts2::Address address;
people.data().UnpackTo(&address);
if(!address.host_address().empty())
std::cout << "家庭地址: " << address.host_address() << std::endl;
if(!address.unit_address().empty())
std::cout << "单位地址: " << address.unit_address() << std::endl;
}
oneof 类型
如果消息中有很多可选字段,并且将来同时只有一个字段会被设置,那么就可以使用 oneof
修饰,也能节约内存
相应格式为:
oneof 字段名 {
字段1 = 编号;
字段2 = 编号;
....
}
注意
:oneof 内的 编号不能和消息中的已有编号重复
比如,我们新增联系人信息一个 “其他联系方式”,从QQ号和微信号二选一
syntax = "proto3";
package contacts;
import "goole/protobuf/any.proto"
//地址
message Address
{
string host_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 {
string qq = 5;
string wechat = 6;
}
}
message Contacts{
repeated PeopleInfo peoples = 1; //多个联系人
}
经编译后:
oneof 的多个字段会被定义为一个枚举类型
设置和获取同上,但注意只能设置一个,如果设置多个,最终结果只保留最后一次设置的信息
清空 oneof 字段:clear_
方法
获取当前设置了哪个字段:_case
方法
write.cc
新增代码
//其他联系方式
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 << "请输入微信号: ";
std::string wechat;
getline(std::cin, wechat);
people->set_wechat(wechat);
}
else
std::cout << "输入有误,无其他联系方式" << std::endl;
注意
:虽然 oneof 可以重复设置,但仍保持唯一性,最终结果为最后一次设置的类型
read.cc
//其他联系方式
//读取方式一
if(people.has_qq())
std::cout << "qq号: " << people.qq() << std::endl;
else if(people.has_wechat())
std::cout << "微信号: " << people.wechat() << std::endl;
//读取方式二
switch (people.other_contact_case())
{
case contacts2::PeopleInfo::OtherContactCase::kQq:
std::cout << "QQ号: " << people.qq() << std::endl;
break;
case contacts2::PeopleInfo::OtherContactCase::kWechat:
std::cout << "微信号: " << people.wechat() << std::endl;
break;
default:
break;
}
map
map也就是哈希表,支持创建关联映射字段,格式为:
map<key_type, value_type> map_field = 字段编号;
注意:
key_type
:除 float 和 bytes 类型以外的任意标量类型。value_type
:可以是任意类型
map 字段不可以用 repeated 修饰
map 中存入的元素是无序的
新增备注字段,有备注标题和备注信息两个部分
syntax = "proto3";
package contacts;
import "goole/protobuf/any.proto"
//地址
message Address
{
string host_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 {
string qq = 5;
string wechat = 6;
}
//备注
map<string, string> remark = 7;
}
message Contacts{
repeated PeopleInfo peoples = 1; //多个联系人
}
对于Map类型字段
- 清空map:clear_ 方法
- 设置和获取:获取方法的方法名称和小写字段名称相同。设置方法为 mutable_ 方法,返回值为 Map 类型的指针,已经开辟好空间,直接对这块空间的内容进行修改即可
- map的遍历可以采用迭代器访问,cbegin()返回起始元素迭代器,cend()返回终止元素迭代器,遍历使用
++
write.cc
:设置数据
//备注信息
for(int i = 0; ;++i)
{
std::cout << "请输入第" << i + 1 << "备注信息标题(仅回车表示结束): ";
std::string remark_info;
getline(std::cin, remark_info);
if(remark_info.empty())
break;
std::cout << "请输入备注信息内容: ";
std::string remark_message;
getline(std::cin, remark_message);
people->mutable_remark()->insert({remark_info, remark_message});
}
read.cc
:迭代器获取数据
//备注信息
for(auto it = people.remark().cbegin(); it != people.remark().cend(); ++it)
std::cout << "备注信息标题: " << it->first << std::endl
<< "备注信息内容: " << it->second << std::endl;
以上就是本篇博客的所有内容,感谢你的阅读
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。