在语法详解部分。这个部分会对通讯录进⾏多次升级,使⽤ 2.x表⽰升级的版本,最终将会升级如下内容:
消息的字段可以⽤下⾯⼏种规则来修饰:
更新 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;
}
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;
}
我们使用嵌套式的作为演示,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类中,这里生成的代码有报错语法提示,是因为插件原因,不用在意,实际上没有任何问题):
以上几种方法是基本message常用方法,而在PeopleInfo中因为声明了Phone字段为 repeated 类型,所以会多几个不同的方法:
这样,带有repeated声明的成员处理方法我们也就清楚了。
对通讯录序列化有三点:
#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
指令来查看二进制序列:
当然,如果你观看别人用PB写的C++程序,可能会看到这样一句宏定义在main函数开头:
GOOGLE_PROTOBUF_VERIFY_VERSION;
用人话来说就是可能使用PB版本不匹配,而版本不匹配是会报错的,所以在启动程序之前添加这个宏可以用来检测使用的PB版本是否一致。
而我们在结束时注释了一个函数:
google::protobuf::ShutdownProtobufLibrary();
我们新创建一个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()
来获取手机号数组。