【ProtoBuf】proto3语法(一)
【ProtoBuf】proto3语法(一)
文章目录
- 【ProtoBuf】proto3语法(一)
- proto3语法解析
- 字段规则
- 消息类型的定义与使用
- 编写通讯录demo
- 新增/读取联系人并进行序列化
- 读取二进制数据并进行反序列化
proto3语法解析
在语法详解部分。这个部分会对通讯录进⾏多次升级,使⽤ 2.x表⽰升级的版本,最终将会升级如下内容:
- 不再打印联系⼈的序列化结果,⽽是将通讯录序列化后并 写⼊⽂件中。
- 从⽂件中将通讯录解析出来,并进⾏打印。
- 新增联系⼈属性,共包括:姓名、年龄、电话信息、地址、其他联系⽅式、备注。
字段规则
消息的字段可以⽤下⾯⼏种规则来修饰:
- singular :消息中可以包含该字段零次或⼀次(不超过⼀次)。 proto3 语法中,字段默认使⽤该规则。
- repeated :消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。可以理解为定义了⼀个数组。
更新 contacts.proto , PeopleInfo 消息中新增 phone_numbers 字段,表⽰⼀个联系⼈有多个号码,可将其设置为 repeated,写法如下:
syntax = "proto3";
package contacts;
message PeopleInfo
{
string name = 1;
int32 age = 2;
repeated string phone_numbers = 3;
}
消息类型的定义与使用
在单个 .proto ⽂件中可以定义多个消息体,且⽀持定义嵌套类型的消息(任意多层)。每个消息体中的字段编号可以重复(不同作用域)。更新 contacts.proto,我们可以将 phone_number 提取出来,单独成为⼀个消息:
syntax = "proto3";
package contacts2;
message Phone
{
string number = 1;
}
// 定义联系人message
message PeopleInfo
{
string name = 1; // 姓名
int32 age = 2; // 年龄
// repeated string phone_numbers = 3; // 手机号码
repeated Phone phone = 3;
}
- 消息类型可作为字段类型使用:
message PeopleInfo
{
string name = 1; // 姓名
int32 age = 2; // 年龄
// repeated string phone_numbers = 3; // 手机号码
message Phone
{
string number = 1;
}
repeated Phone phone = 3;
}
- 可以导入其他 .proto 文件的消息类型并使用:
Phone消息定义在phone.proto中:
syntax = "proto3";
package phone;
message Phone
{
string number = 1;
}
如果想要在contacts.proto文件中使用Phone message类型字段,则:
syntax = "proto3";
package contacts2;
import "Phone.proto";// 使用import 将 Phone.proto 文件导入进来
// 定义联系人message
message PeopleInfo
{
string name = 1; // 姓名
int32 age = 2; // 年龄
repeated phone.Phone payphone = 3;
}
- 注意:在proto3文件中可以导入proto2消息类型并使用它们,反之亦然。
我们使用嵌套式的作为演示,PeopleInfo里的字段已经差不多了,这样每个人的信息结构也就出来了,随后我们应该定义一个message 作为通讯录,通讯录里是一个数组,数组每一个元素都是一个PeopleInfo:
syntax = "proto3";
package contacts2;
// 定义联系人message
message PeopleInfo
{
string name = 1; // 姓名
int32 age = 2; // 年龄
message Phone
{
string number = 1;
}
repeated Phone phone = 3;// 电话信息
}
// 通讯录message
message Contacts
{
repeated PeopleInfo contacts = 1;
}
我们来看一下生成的cpp代码中有什么:
当然,我们的关注点不在这里,我们进入到一个类当中(PeopleInfo_Phone类中,这里生成的代码有报错语法提示,是因为插件原因,不用在意,实际上没有任何问题):
- clear_number():对字段进行清空
- number():省略了get前缀,实际是get方法,获取number字段
- set_number():设置number字段
以上几种方法是基本message常用方法,而在PeopleInfo中因为声明了Phone字段为 repeated 类型,所以会多几个不同的方法:
这样,带有repeated声明的成员处理方法我们也就清楚了。
编写通讯录demo
新增/读取联系人并进行序列化
对通讯录序列化有三点:
- 读取本地已存在的联系人文件(不存在则创建该文件)
#define FILE_SAVE "contacts.bin"// 文件保存的文件名
int main()
{
contacts2::Contacts contacts; // 定义通讯录方法
// 1. 读取本地已存在的联系人文件
fstream input(FILE_SAVE, ios::in | ios::binary);
if(!input)
{
cout << "contacts.bin not found, create new file!" << endl;
}
else if(contacts.ParseFromIstream(&input))
{
cerr << "parse error!" << endl;
input.close();
return -1;
}
return 0;
}
contacts2为contacts.pb.h中的命名空间,由于PB序列化之后是二进制文件,所以从文件中读取数据使用ios::binary,读取成功之后,通过contacts.pb.h中提供的解析二进制序列方法,将通讯录数据序列化为二进制文件,并保存在 contacts.bin
文件中。
- 向通讯录中添加一个联系人
// 联系人信息类型为PeopleInfo类型
void AddPeopleInfo(contacts2::PeopleInfo* people)
{
cout << "---------------新增联系人---------------" << endl;
cout << "请输入联系人姓名: ";
string name;
getline(cin, name);
people->set_name(name);// 调用set方法设置联系人名称
cout << "请输入联系人年龄: ";
int age;
cin >> age;
people->set_age(age);// 调用set方法设置联系人年龄
cin.ignore(256, '\n');// 防止\n影响下次输入
for(int i = 0;;i++)
{
cout << "请输入联系人电话" << i + 1 << "(只输入回车完成电话新增): ";
string number;
getline(cin, number);
if(number.empty())
{
break;
}
contacts2::PeopleInfo_Phone* phone = people->add_phone();
phone->set_number(number);
}
cout << "-------------添加联系人成功--------------" << endl;
}
int main()
{
// ...
// 2. 向通讯录中添加一个联系人
AddPeopleInfo(contacts.add_contacts());
return 0;
}
使用 cin.ignore(256, '\n')
接口,目的是为了不让’\n’影响到接下来的输入。首先来解释第二个参数,第二个参数的含义是遇到 ‘\n’ 则会停止清空缓冲区(前面的内容全部清除,包括\n)。第一个参数的含义是如果清除了256个字符后还没有遇到 ‘\n’,则也会停下来不再清楚缓冲区。
之后就进行循环输入电话号码,如果想要停止输入(输入为空),则按两次回车即可终止。而由于电话信息是一个数组,所以一个电话信息在存储之前需要先添加一个数组元素,即调用add_phone()函数,然后再调用set函数将新输入的电话号码保存到数组中。
- 将通讯录写入到本地文件中
int main()
{
// 3. 将通讯录写入到本地文件中
fstream output(FILE_SAVE, ios::out | ios::trunc | ios::binary);
if(!contacts.SerializeToOstream(&output))
{
cerr << "write error!" << endl;
input.close();
output.close();
return -1;
}
cout << "write success!" << endl;
input.close();
output.close();
return 0;
}
最后将通讯录写入到本地文件中,写入之前我们先进行序列化,依旧使用PB提供的序列化函数来进行序列化:
编写makefile对通讯录项目做编译:
write:write.cc contacts.pb.cc
g++ -o $@ $^ -std=c++11 -lprotobuf
.PHONY:clean
clean:
rm -f write
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
#define FILE_SAVE "contacts.bin"// 文件保存的文件名
void AddPeopleInfo(contacts2::PeopleInfo* people)
{
cout << "---------------新增联系人---------------" << endl;
cout << "请输入联系人姓名: ";
string name;
getline(cin, name);
people->set_name(name);
cout << "请输入联系人年龄: ";
int age;
cin >> age;
people->set_age(age);
cin.ignore(256, '\n');
for(int i = 0;;i++)
{
cout << "请输入联系人电话" << i + 1 << "(只输入回车完成电话新增): ";
string number;
getline(cin, number);
if(number.empty())
{
break;
}
contacts2::PeopleInfo_Phone* phone = people->add_phone();
phone->set_number(number);
}
cout << "-------------添加联系人成功--------------" << endl;
}
int main()
{
contacts2::Contacts contacts; // 定义通讯录方法
// 1. 读取本地已存在的联系人文件
fstream input(FILE_SAVE, ios::in | ios::binary);
if(!input)
{
cout << "contacts.bin not found, create new file!" << endl;
}
else if(contacts.ParseFromIstream(&input))
{
cerr << "parse error!" << endl;
input.close();
return -1;
}
// 2. 向通讯录中添加一个联系人
AddPeopleInfo(contacts.add_contacts());
// 3. 将通讯录写入到本地文件中
fstream output(FILE_SAVE, ios::out | ios::trunc | ios::binary);
if(!contacts.SerializeToOstream(&output))
{
cerr << "write error!" << endl;
input.close();
output.close();
return -1;
}
cout << "write success!" << endl;
input.close();
output.close();
// google::protobuf::ShutdownProtobufLibrary();
return 0;
}
接下来就使用make来编译文件,然后运行程序:
我们发现生成了这个文件,并且记录着我们之前输入的数据,不过这里格式不对,因为是序列化为了二进制文件,而我们不能直接读取二进制文件,我们可以通过 hexdump
指令来查看二进制序列:
- hexdump :用来查看二进制序列的命令,有不同的选项可以将二进制序列以十六进制形式进行输出。
- C选项: 以标准格式显示包含地址和 ASCII 值的内容。每一行显示 16 字节的十六进制数字以及对应的 ASCII 字符。
- n <数值>选项: 仅输出前 <数值> 字节。
- v选项: 显示所有字节,包括重复的字节。
当然,如果你观看别人用PB写的C++程序,可能会看到这样一句宏定义在main函数开头:
GOOGLE_PROTOBUF_VERIFY_VERSION;
- GOOGLE_PROTOBUF_VERIFY_VERSION : 验证没有意外链接到与编译的头⽂件不兼容的库版本。如果检测到版本不匹配,程序将中⽌。注意,每个 .pb.cc ⽂件在启动时都会⾃动调⽤此宏。在使⽤ C++ Protocol Buffer 库之前执⾏此宏是⼀种很好的做法,但不是绝对必要的。
用人话来说就是可能使用PB版本不匹配,而版本不匹配是会报错的,所以在启动程序之前添加这个宏可以用来检测使用的PB版本是否一致。
而我们在结束时注释了一个函数:
google::protobuf::ShutdownProtobufLibrary();
- google::protobuf::ShutdownProtobufLibrary() : 在程序结束时调⽤这个接口,是为了删除 Protocol Buffer 库分配的所有全局对象。对于⼤多数程序来说这是不必要的,因为该过程⽆论如何都要退出,并且操作系统将负责回收其所有内存。但是,如果你使⽤了内存泄漏检查程序,该程序需要释放每个最后对象,或者你正在编写可以由单个进程多次加载和卸载的库,那么你可能希望强制使⽤ Protocol Buffers 来清理所有内容。
读取二进制数据并进行反序列化
我们新创建一个read.cc文件用来从contacts.bin文件中进行读取二进制数据并进行反序列化解析,将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
接下来对本地通讯录的操作有两步:
- 读取本地已经存在的通讯录文件
#define FILE_READ "contacts.bin"// 文件保存的文件名
int main()
{
contacts2::Contacts contacts; // 定义通讯录对象
// 1. 读取本地已存在的通讯录文件
fstream input(FILE_READ, ios::in | ios::binary);
if(!contacts.ParseFromIstream(&input))
{
cerr << "parse error!" << endl;
input.close();
return -1;
}
return 0;
}
来解析通讯录中的消息,与write.cc开始相同
- 打印通讯录列表
void PrintContacts(contacts2::Contacts& contacts)
{
for(int i = 0; i < contacts.contacts_size(); ++i)
{
cout << "-----------------联系人" << i + 1 << "-----------------" << endl;
const contacts2::PeopleInfo& people = contacts.contacts(i);
cout << "联系人姓名: " << people.name() << endl;
cout << "联系人年龄: " << people.age() << endl;
for(int j = 0; j < people.phone_size(); ++j)
{
const contacts2::PeopleInfo_Phone& phone = people.phone(j);
cout << "联系人电话" << j + 1 << " : " << phone.number() << endl;
}
cout << "-----------------通讯录尾-----------------" << endl;
}
}
int main()
{
// ...
// 2. 打印通讯录列表
PrintContacts(contacts);
return 0;
}
由于通讯录message中只有一个PeopleInfo字段并且是数组类型,所以我们直接使用contacts()来获取数组中的元素值,即每一个联系人的基本信息。然后打印出联系人的姓名和年龄信息。
而每个人的手机号信息都是一个数组,所以我们需要对每一个联系人的手机号数组进行遍历,将所有手机号给打印出来,因为phone也是一个数组,所以可以直接调用 people.phone()
来获取手机号数组。