目录
为了具体讲解 proto3语法,将通讯录的需求变化为:
-
不再打印联系人的序列化结果,而是将通讯录序列化后并写入文件中
-
从文件中将通讯录解析出来,并进行打印
-
新增联系人属性,共包括:姓名、年龄、电话信息、地址、其他联系方式、备注
下面将使用不同的 proto3 语法,将上述需求依次实现,帮助我们更好的理解 proto语法
字段规则和消息类型的定义与使用
消息的字段可以用下面几种规则来修饰:
-
singular:消息中可以包含该字段零次或一次(不超过一次),proto3 语法中,字段默认使用该规则
-
repeated:消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。可以理解为定义了一个数组
现在新增电话号码的联系人属性,因为一个人可能有多个电话号码,所以需要加 repeated,表示可以由多个该字段
方式一
方式二
另一种方式是,将电话信息单独定义一个类,也就是新增一个 message:
不同的 message,字段编号可以从1开始
方式三
方式二是在 PeopleInfo外面定义 Phone,也可以嵌套定义,也就是在 PeopleInfo 里面定义:
同样嵌套定义的 message,字段编号可以从1开始
方式四
上面的几种方式都是在同一个 .proto文件 中定义的message,也可以在不同的文件,例如创建一个 phone.proto,里面创建 message Phone:
此时在 contacts.proto文件 中,就需要 import 引入 phone.proto,并在使用 Phone 时,加上 package 指定的 phone 命名空间,如果 phone.proto 中没有写 package,这里就不需要加了:
编译.proto文件
在通讯录2.0版本,只新增联系人属性,共包括:姓名、年龄、电话信息
首先采用嵌套定义的方式,新增 电话信息:
又因为需求是将一个 通讯录 序列化后写入文件中,而此时只定义了 联系人message,并没有定义 通讯录message,所以新增 通讯录message:
此时编译 proto文件:
会生成对应的头文件和源文件:
又因为这个 proto文件 中,定义了三个 message,所以在生成的头文件中会有相应的类:
因为在 PeopleInfo 中有类似于数组的成员,所以在编译后,针对这个成员肯定有类似的插入数组的方法:
同样通讯录类中也有类似的方法:
此时的 contacts.proto文件:
syntax = "proto3";
package contact2;
// 定义联系人 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;
}
实现通讯录2.0版本
将通讯录序列化后写入文件中
下面实现将通讯录序列化后写入文件中,分为下面三步:
-
读取本地已存在的通讯录文件
-
向通讯录中只添加一个联系人属性(其余属性后续添加)
-
将通讯录序列化后并写入文件中的功能
首先创建 makefile 和 write.cc,方便编译:
makefile:
write:write.cc contacts.pb.cc
g++ -o $@ $^ -std=c++11 -lprotobuf
.PHONY:clean
clean:
rm -f write
write.cc:
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
void AddPeopleInfo(contact2::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 = 1; ; i++)
{
cout << "请输入联系人电话" << i << "(只输入回车表示输入完毕): ";
string number;
getline(cin, number);
if(number.empty())
{
break;
}
contact2::PeopleInfo_Phone* phone = people->add_phone();
phone->set_number(number);
}
cout << "-----------添加联系人成功-----------" << endl;
}
int main()
{
contact2::Contacts contacts;
// 1. 读取本地已存在的通讯录文件
fstream input("contacts.bin", ios::in | ios::binary);
if(!input)
{
cout << "contacts.bin不存在, 创建了一个新的该文件" << endl;
}
else if(!contacts.ParseFromIstream(&input))
{
cerr << "反序列化失败" << endl;
input.close();
return -1;
}
// 2. 向通讯录中添加一个联系人
AddPeopleInfo(contacts.add_contacts());
// 3. 将通讯录序列化后并写入文件中
fstream output("contacts.bin", ios::out | ios::trunc | ios::binary);
if(!contacts.SerializeToOstream(&output))
{
cerr << "序列化失败" << endl;
input.close();
output.close();
return -1;
}
cout << "序列化成功" << endl;
return 0;
}
write.cc 代码中的注意事项:
-
因为 ProtoBuf 序列化的结果是二进制字节序列,所以 input 需要传入ios::binary
-
程序中任意地方如果出现失败的情况,都需要将 input/output close掉
-
output 之所以传入 ios::trunc 覆盖写入,是因为在最开始就已经读取本地文件了
-
ParseFromIstream方法实现了先读取本地文件,再将数据进行反序列化操作
-
SerializeToOstream方法实现了将通讯录对象序列化,并将序列化对象写入文件中
-
AddPeopleInfo函数传入自动生成的 add_contacts函数,是利用该函数的返回值添加内容
-
cin.ignore(256, '\n') 表示清除输入缓冲区中的 \n 字符就结束,或是清除了 256个字符就结束,这句代码是为了消除输入完年龄后的回车键,避免影响接下来的号码的输入,
-
AddPeopleInfo函数中插入电话号码时,同样利用 add_phone函数 的返回值插入
此时执行 make,再运行生成的 write,输入后就添加成功:
就会生成一个 contacts.bin 的二进制文件:
因为是二进制文件,所以会出现一些乱码的情况,到此完成联系人的添加,并序列化后写入文件中了
可以使用 hexdump工具 查看二进制文件,将其转化为 16进制 和 对应的ASCII码,执行:
hexdump -C contacts.bin
将文件中通讯录对象反序列化并打印
将文件中通讯录对象反序列化并打印共以下两步:
-
读取本地已存在的通讯录文件
-
打印通讯录列表
此时新增 read.cc 和 改变Makefile:
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
read.cc:
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
void PrintContacts(contact2::Contacts& contacts)
{
for(int i = 0; i < contacts.contacts_size(); i++)
{
cout << "-----------联系人" << i+1 << "-----------" << endl;
const contact2::PeopleInfo& people = contacts.contacts(i);
cout << "联系人姓名: " << people.name() << endl;
cout << "联系人年龄: " << people.age() << endl;
for(int j = 0; j < people.phone_size(); j++)
{
const contact2::PeopleInfo_Phone& phone = people.phone(j);
cout << "联系人电话" << j+1 << ":" << phone.number() << endl;;
}
}
}
int main()
{
contact2::Contacts contacts;
// 1. 读取本地已存在的通讯录文件
fstream input("contacts.bin", ios::in | ios::binary);
if(!contacts.ParseFromIstream(&input))
{
cerr << "反序列化失败" << endl;
input.close();
return -1;
}
// 2. 打印通讯录列表
PrintContacts(contacts);
return 0;
}
read.cc 代码中的注意事项:
-
读取本地已存在的通讯录文件与 write.cc 中的实现方式是一样的
-
传入 PrintContacts函数 中的通讯录对象是反序列化好的数组,只需要遍历该数组即可
-
contacts_size() 和 phone_size() 是自动生成的返回数组中元素个数的函数
-
contacts.contacts(i) 代码的含义是:返回 contacts数组 中下标为 i 的对象
该函数的返回值是:const contact2::PeopleInfo&,用于接收该下标对应的对象
打印联系人电话时的 people.phone(j) 同理
此时执行 make,再运行生成的 read,输入后就成功打印刚刚添加的联系人信息:
成功完成将文件中的通讯录对象反序列化并打印
--decode命令查看二进制文件
--decode 是 ProtoBuf 中的一种命令选项,使用 protoc -h 可以查看所有的命令选项:
其中就有--decode,--decode 是从 标准输入 中读取二进制消息,但是我们希望是从文件中读取的,所以使用输入重定向到 contacts.bin 文件中:
此时就能看到二进制消息的内容了
其中 name 后面的就是将 utf8 汉字,转化为 8进制 的格式了
所以如果想查看二进制消息,直接执行 --decode 的命令就能够查看二进制的消息,比上面代码的方式更简便
enum类型
proto3语法 还支持我们定义一个枚举类型的字段:
注意事项:
-
0值常量必须存在,并且必须要作为枚举类型的第一个元素的枚举值
-
枚举常量值必须在 32位 整数范围内,不能设置为负值
-
枚举类型能够定义在 message 内部
-
同级的枚举类型下,各个枚举的枚举类型中,不能有相同的常量名称
-
如果依赖了其他的 .proto文件,这两个文件中如果都没有声明不同的 package 的话,两个文件中也不能有相同的常量名称 如果两个文件中想使用相同的常量名称,解决方式就是加上 package 即可
实现通讯录2.1版本
学习了 enum类型 后,就可以对 通讯录2.0版本 做以更新,可以将电话信息分为 固定电话(TEL)和移动电话(MP),所以此时的 proto文件 更新为:
在执行完 protoc --cpp_out=. contacts.proto 后,生成的 contacts.pb.h 中就有相应的方法了,例如与 type 相对的,就有 clear、get、set 相关方法:
下面实现在 write.cc 中,添加一个联系人时,设置联系人的类型
也就是在 write.cc 中的 AddPeopleInfo函数 中,输入联系人电话后的部分,加上电话类型:
write.cc 改为:
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
void AddPeopleInfo(contact2::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 = 1; ; i++)
{
cout << "请输入联系人电话" << i << "(只输入回车表示输入完毕): ";
string number;
getline(cin, number);
if(number.empty())
{
break;
}
contact2::PeopleInfo_Phone* phone = people->add_phone();
phone->set_number(number);
cout << "请输入该电话类型(1.移动电话 2.固定电话)";
int type;
cin >> type;
cin.ignore(256, '\n');
switch(type)
{
case 1:
phone->set_type(contact2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
break;
case 2:
phone->set_type(contact2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);
break;
default:
cout << "输入有误!" << endl;
break;
}
}
cout << "-----------添加联系人成功-----------" << endl;
}
int main()
{
contact2::Contacts contacts;
// 1. 读取本地已存在的通讯录文件
fstream input("contacts.bin", ios::in | ios::binary);
if(!input)
{
cout << "contacts.bin不存在, 创建了一个新的该文件" << endl;
}
else if(!contacts.ParseFromIstream(&input))
{
cerr << "反序列化失败" << endl;
input.close();
return -1;
}
// 2. 向通讯录中添加一个联系人
AddPeopleInfo(contacts.add_contacts());
// 3. 将通讯录序列化后并写入文件中
fstream output("contacts.bin", ios::out | ios::trunc | ios::binary);
if(!contacts.SerializeToOstream(&output))
{
cerr << "序列化失败" << endl;
input.close();
output.close();
return -1;
}
cout << "序列化成功" << endl;
return 0;
}
同时 read.cc 也需要更新,在读取时将电话类型也读取出来:
格式为:联系人电话1:123456 (MP)
注意:
-
phone.type() 可以得到电话的类型,但是得到的是 int 类型的值
-
phone.PhoneType_Name() 可以根据得到的 int 类型的值打印出 对应的 MP 或 TEL
此时 read.cc 代码变为了:
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
void PrintContacts(contact2::Contacts& contacts)
{
for(int i = 0; i < contacts.contacts_size(); i++)
{
cout << "-----------联系人" << i+1 << "-----------" << endl;
const contact2::PeopleInfo& people = contacts.contacts(i);
cout << "联系人姓名: " << people.name() << endl;
cout << "联系人年龄: " << people.age() << endl;
for(int j = 0; j < people.phone_size(); j++)
{
const contact2::PeopleInfo_Phone& phone = people.phone(j);
cout << "联系人电话" << j+1 << ":" << phone.number();
// 格式为: 联系人电话1:123456 (MP)
cout << " (" << phone.PhoneType_Name(phone.type()) << ")" << endl;
}
}
}
int main()
{
contact2::Contacts contacts;
// 1. 读取本地已存在的通讯录文件
fstream input("contacts.bin", ios::in | ios::binary);
if(!contacts.ParseFromIstream(&input))
{
cerr << "反序列化失败" << endl;
input.close();
return -1;
}
// 打印通讯录列表
PrintContacts(contacts);
return 0;
}
此时改动完 write.cc 和 read.cc 后,make 编译这两个文件,先新增一个联系人李四,此时就会多出一步,询问电话类型是哪一种:
下面读取联系人:
可以看到刚刚新增的李四设为了 固定电话TEL,而之前增加的 张三 的两个号码都默认为 移动电话MP,这就是枚举类型的默认值
在读取文件中联系人时,ProtoBuf 会对没有指定类型的电话,设置一个默认值,默认值就是常量值为 0 的枚举常量,也就是 MP
通讯录2.1版本实现完毕
Any类型
any 是存在于 /usr/local/protobuf/ 下的 include/google/protobuf/any.proto 文件中的
下面使用 cat 查看这个文件:
可以得知:
-
Any 其实就是一个 message
-
Any 有两个属性,分别是 type_url 和 value
-
Any 是在 google.protobuf 的 package 中,所以使用 Any 时格式为:google.protobuf.Any
-
Any 类型字段中可以存储任意的消息类型
所以想使用Any时,就可以直接将字段设置为 Any类型了
接下来就可以实现 实现通讯录2.2版本了,新增一个地址属性,类型就是 Any
实现通讯录2.2版本
先在 contacts.proto 中添加地址属性:
改变后的 contacts.proto文件为:
syntax = "proto3";
package contact2;
import "google/protobuf/any.proto";
message Address
{
string home_address = 1; // 家庭地址
string unit_address = 2; // 单位地址
}
// 定义联系人 message
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
message Contacts
{
repeated PeopleInfo contacts = 1;
}
了解生成的方法
此时 protoc 编译 contacts.proto文件,查看 contacts.pb.h 中新增的信息,可以看到新增的 Address类中的 home_address 和 unit_address 有各自的 clear、get、set方法:
在 PeopleInfo类 中,Any字段 data 相关的方法有:
-
has_data()方法 用于判断当前的消息是否设置过data,返回值是 bool
-
data()方法 也就是 get方法,返回值是 ProtoBuf 定义好的类型: PROTOBUF_NAMESPACE_ID::Any&
-
mutable_data()方法也就是 set方法,返回值是一个空间的地址 PROTOBUF_NAMESPACE_ID::Any* 也就是 ProtoBuf 已经帮我们开辟好了一块空间,我们只需要在这块空间上进行一定的操作即可
想要在 Any类型 中存放我们的地址信息,而地址信息在 C++文件中是一个类,所以下面需要明白:如何让 Any类型 存放 地址信息,也就是 Class Address
所以打开 Any类型,查看一些方法:
-
PackFrom 和 UnpackTo 函数,它们的方法都是 Message类型的,Message是我们自定义的 Message 的父类
-
PackFrom方法,可以将之前设置的任意消息类型,转换为 Any类型
-
UnpackTo方法,可以将 Any类型,转换为之前设置的任意消息类型,结果放在参数的地址中
-
Is方法, 用于判断 Any 字段中存放的消息类型,是否为 T类型,返回值是 bool
正式实现通讯录2.2版本
在上面定义了 Any类型的 data,也定义了 message Address
在 write.cc 中,输入完地址后,调用 PackFrom 先将 Address类型 转换为 Any类型
此时完整的 write.cc文件 为:
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
void AddPeopleInfo(contact2::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 = 1; ; i++)
{
cout << "请输入联系人电话" << i << "(只输入回车表示输入完毕): ";
string number;
getline(cin, number);
if(number.empty())
{
break;
}
contact2::PeopleInfo_Phone* phone = people->add_phone();
phone->set_number(number);
cout << "请输入该电话类型(1.移动电话 2.固定电话)";
int type;
cin >> type;
cin.ignore(256, '\n');
switch(type)
{
case 1:
phone->set_type(contact2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
break;
case 2:
phone->set_type(contact2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);
break;
default:
cout << "输入有误!" << endl;
break;
}
}
// 输入地址信息
contact2::Address address;
cout << "请输入联系人家庭地址: ";
string home_address;
getline(cin, home_address);
address.set_home_address(home_address);
cout << "请输入联系人单位地址: ";
string unit_address;
getline(cin, unit_address);
address.set_unit_address(unit_address);
// Address -> Any
people->mutable_data()->PackFrom(address);
cout << "-----------添加联系人成功-----------" << endl;
}
int main()
{
contact2::Contacts contacts;
// 1. 读取本地已存在的通讯录文件
fstream input("contacts.bin", ios::in | ios::binary);
if(!input)
{
cout << "contacts.bin不存在, 创建了一个新的该文件" << endl;
}
else if(!contacts.ParseFromIstream(&input))
{
cerr << "反序列化失败" << endl;
input.close();
return -1;
}
// 2. 向通讯录中添加一个联系人
AddPeopleInfo(contacts.add_contacts());
// 3. 将通讯录序列化后并写入文件中
fstream output("contacts.bin", ios::out | ios::trunc | ios::binary);
if(!contacts.SerializeToOstream(&output))
{
cerr << "序列化失败" << endl;
input.close();
output.close();
return -1;
}
cout << "序列化成功" << endl;
return 0;
}
所以接着在 read.cc 中,打印完电话信息后,就可以对地址信息进行一些打印了:
先调用 has_data() 和 Is() 方法,如果都满足,再 调用 UnoackTo 方法,将 Any类型 转化为 Address类型 再打印
此时完整的 read.cc文件 为:
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
void PrintContacts(contact2::Contacts& contacts)
{
for(int i = 0; i < contacts.contacts_size(); i++)
{
cout << "-----------联系人" << i+1 << "-----------" << endl;
const contact2::PeopleInfo& people = contacts.contacts(i);
cout << "联系人姓名: " << people.name() << endl;
cout << "联系人年龄: " << people.age() << endl;
for(int j = 0; j < people.phone_size(); j++)
{
const contact2::PeopleInfo_Phone& phone = people.phone(j);
cout << "联系人电话" << j+1 << ":" << phone.number();
// 格式为: 联系人电话1:123456 (MP)
cout << " (" << phone.PhoneType_Name(phone.type()) << ")" << endl;
}
// 判断people中data是否设置过类型,如果设置过,设置的类型是否是 Address
if(people.has_data() && people.data().Is<contact2::Address>())
{
contact2::Address address;
// 将Any类型转化为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;
}
}
}
}
int main()
{
contact2::Contacts contacts;
// 1. 读取本地已存在的通讯录文件
fstream input("contacts.bin", ios::in | ios::binary);
if(!contacts.ParseFromIstream(&input))
{
cerr << "反序列化失败" << endl;
input.close();
return -1;
}
// 打印通讯录列表
PrintContacts(contacts);
return 0;
}
下面 make 编译后,新增联系人 王五,并设置他的地址信息:
下面读取文件:
至此使用 Any类型 新增 联系人地址 的版本就实现完成了
oneof类型
接下来使用 oneof类型,新增联系人的 qq 和 微信号
这里使用 oneof类型,就是为了做到 要么使用 qq,要么使用微信号
-
oneof 中的字段编号不能和外面的字段编号重复
-
oneof 中不能使用 repeated 修饰
-
oneof 中徐国设置了多个字段,只会保留最后一次设置的字段
contacts.proto文件 修改如下:
编译后,观察 contacts.pb.h 中新增的方法,都有has、clear、get、set方法:
-
qq和wechat的 has方法,都是判断是否设置了该字段
-
clear_other_contact方法 是清除 oneof 字段
-
other_contact_case方法 通过返回值就知道设置的哪个字段,
返回值是枚举类型,新增一个常量0,表示没有设置: -
实现通讯录2.3版本
下面在 write.cc 中新增联系人的其他联系方式:
read.cc 也添加读取其他联系方式的逻辑:
下面编译 write.cc 和 read.cc,并新增联系人赵六:
读取联系人信息:
新增 联系人其他联系方式 的版本就实现完成了
map类型
下面使用 oneof类型,新增联系人的 备注信息
有三个注意点:
-
map 的Key支持除了 float和bytes 之外的类型,Value支持所有类型
-
map 不能使用 repeated字段修饰
-
map 中设置多多个键值对,设置的元素是无序的
对 contacts.proto文件 编译后,观察对 map字段 生成的方法:
实现通讯录2.4版本
首先在 write.cc 中新增备注信息:
write.cc 完成代码为:
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
void AddPeopleInfo(contact2::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 = 1; ; i++)
{
cout << "请输入联系人电话" << i << "(只输入回车表示输入完毕): ";
string number;
getline(cin, number);
if(number.empty())
{
break;
}
contact2::PeopleInfo_Phone* phone = people->add_phone();
phone->set_number(number);
cout << "请输入该电话类型(1.移动电话 2.固定电话): ";
int type;
cin >> type;
cin.ignore(256, '\n');
switch(type)
{
case 1:
phone->set_type(contact2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
break;
case 2:
phone->set_type(contact2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);
break;
default:
cout << "输入有误!" << endl;
break;
}
}
// 输入地址信息
contact2::Address address;
cout << "请输入联系人家庭地址: ";
string home_address;
getline(cin, home_address);
address.set_home_address(home_address);
cout << "请输入联系人单位地址: ";
string unit_address;
getline(cin, unit_address);
address.set_unit_address(unit_address);
// Address -> Any
people->mutable_data()->PackFrom(address);
cout << "请选择要添加的其他联系方式(1.qq 2.微信): ";
int other_contact;
cin >> other_contact;
cin.ignore(256, '\n');
if(1 == other_contact)
{
cout << "请输入联系人qq号: ";
string qq;
getline(cin, qq);
people->set_qq(qq);
}
else if(2 == other_contact)
{
cout << "请输入联系人微信号: ";
string wechat;
getline(cin, wechat);
people->set_wechat(wechat);
}
else
{
cout << "选择有误, 未成功设置其他联系方式!" << endl;
}
for(int i = 0; ; i++)
{
cout << "请输入备注" << i+1 << "标题(只输入回车结束备注新增): ";
string remark_key;
getline(cin, remark_key);
if(remark_key.empty())
{
break;
}
cout << "请输入备注" << i+1 << "内容: ";
string remark_value;
getline(cin, remark_value);
people->mutable_remark()->insert({remark_key, remark_value});
}
cout << "-----------添加联系人成功-----------" << endl;
}
int main()
{
contact2::Contacts contacts;
// 1. 读取本地已存在的通讯录文件
fstream input("contacts.bin", ios::in | ios::binary);
if(!input)
{
cout << "contacts.bin不存在, 创建了一个新的该文件" << endl;
}
else if(!contacts.ParseFromIstream(&input))
{
cerr << "反序列化失败" << endl;
input.close();
return -1;
}
// 2. 向通讯录中添加一个联系人
AddPeopleInfo(contacts.add_contacts());
// 3. 将通讯录序列化后并写入文件中
fstream output("contacts.bin", ios::out | ios::trunc | ios::binary);
if(!contacts.SerializeToOstream(&output))
{
cerr << "序列化失败" << endl;
input.close();
output.close();
return -1;
}
cout << "序列化成功" << endl;
return 0;
}
read.cc 也新增读取备注信息的逻辑:
read.cc 完成代码为:
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
void PrintContacts(contact2::Contacts& contacts)
{
for(int i = 0; i < contacts.contacts_size(); i++)
{
cout << "-----------联系人" << i+1 << "-----------" << endl;
const contact2::PeopleInfo& people = contacts.contacts(i);
cout << "联系人姓名: " << people.name() << endl;
cout << "联系人年龄: " << people.age() << endl;
for(int j = 0; j < people.phone_size(); j++)
{
const contact2::PeopleInfo_Phone& phone = people.phone(j);
cout << "联系人电话" << j+1 << ":" << phone.number();
// 格式为: 联系人电话1:123456 (MP)
cout << " (" << phone.PhoneType_Name(phone.type()) << ")" << endl;
}
// 判断people中data是否设置过类型,如果设置过,设置的类型是否是 Address
if(people.has_data() && people.data().Is<contact2::Address>())
{
contact2::Address address;
// 将Any类型转化为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;
}
}
switch(people.other_contact_case())
{
case contact2::PeopleInfo::OtherContactCase::kQq:
cout << "联系人qq: " << people.qq() << endl;
break;
case contact2::PeopleInfo::OtherContactCase::kWechat:
cout << "联系人微信: " << people.wechat() << endl;
break;
default:
break;
}
if(people.remark_size())
{
cout << "备注信息: " << endl;
}
for(auto it = people.remark().cbegin(); it != people.remark().cend(); it++)
{
cout << " " << it->first << ": " << it->second << endl;
}
}
}
int main()
{
contact2::Contacts contacts;
// 1. 读取本地已存在的通讯录文件
fstream input("contacts.bin", ios::in | ios::binary);
if(!contacts.ParseFromIstream(&input))
{
cerr << "反序列化失败" << endl;
input.close();
return -1;
}
// 打印通讯录列表
PrintContacts(contacts);
return 0;
}
编译并运行 write,新增联系人田七:
接下来读取联系人:
通讯录2.4版本实现完毕
默认值
反序列化消息时,如果被反序列化的二进制序列中不包含某个字段,反序列化对象中相应字段时,就会设置为该字段的默认值
不同的类型对应的默认值不同:
-
对于字符串,默认值为空字符串
-
对于字节,默认值为空字节
-
对于布尔值,默认值为 false
-
对于数值类型,默认值为 0
-
对于枚举,默认值是第一个定义的枚举值,必须为 0
-
对于消息字段,未设置该字段。它的取值是依赖于语言
-
对于设置了 repeated 的字段的默认值是空的 (通常是相应语言的一个空列表)
-
对于 消息字段、oneof字段 和 any字段 ,C++ 和 Java 中都有 has 方法来检测当前字段是否被设置
对于标量数据类型,在proto3语法下,没有生成 has方法
例如:一个 message 中有三个字段 a、b、c,都是 int32类型的,这时有一个已经被序列化过的二进制序列消息,只有a、b两个字段被设置了,a = 1,b=2,c字段并没有被设置
下面对这一份消息进行反序列化操作,会在内存中生成反序列化内存对象,这个对象中包含了三个字段:
a = 1、b=2、c=0(0是默认值)
这里我们反序列化后,得到 c 的值是0,我们不确定这里的0,是我们自己设置的,还是反序列化时给的默认值
所以如果有 has方法,就可以判断这里的 c=0 到底是自己设置的还是默认值,但是标量数据类型是没有 has方法的
其实这种情况也不是大问题,在大部分的业务场景下,是可以兼容默认值的,比如说这里的c表示银行卡余额,那么我们设置为0和默认值为0,其实意义都是一样的
更新消息
更新消息包括:新增、修改、删除
新增:
注意不要和老字段冲突即可,包括名称和字段编号
修改:
-
禁止修改任何已有字段的字段编号
-
int32, uint32,int64, uint64和 bool是完全兼容的,可以从这些类型中的一个改为另一个而不破坏前后兼容性。若解析出来的数值与相应的类型不匹配,会采用与 C++一致的处理方案(例如,若将 64 位整数当做 32 位进行读取,它将被截断为 32 位)
-
sint32 和 sint64 相互兼容但不与其他的整型兼容
-
string 和 bytes 在合法 UTF-8 字节前提下也是兼容的
-
bytes 包含消息编码版本的情况不,嵌套消息与 bytes 也是兼容的
-
fixed64 与 sfixed64兼容,fixed32 与 sfixed32 兼容
-
enum 与 int32,uint32,int64和 uint64兼容(注意若值不匹配会被截断),但要注意当反序列化消息时会根据语言采用不同的处理方案
例如,未识别的 proto3 枚举类型会被保存在消息中,但是当消息反序列化时如何表示是依赖于编程语言的。整型字段总是会保持其的值 -
oneof:
将一个单独的值更改为 新 oneof 类型成员之一是安全和二进制兼容的
若确定没有代码一次性设置多个值那么将多个字段移入一个新 oneof 类型也是可行的
将任何字段移入已存在的 oneof 类型是不安全的。
删除:
如果字段编号2原本表示的是生日,我们将这个字段删除,新的字段编号2表示年龄,此时就会混淆,会出问题
如果要删除老字段,要保证不使用已经被删除的或者已经被注释掉的字段编号
reserved
为了保证不使用已经被删除的或者已经被注释掉的字段编号,ProtoBuf 引入了 reserved 关键字,能够指定字段编号为保留项,如果以后使用被保留的字段编号,ProtoBuf 就会产生告警
如果代码如下所示:
此时使用 ProtoBuf 编译,就会告警 字段编号2 是被保留的,建议使用 字段编号4:
-
可以 reserved 2 这样单独保留字段编号
-
可以 reserved 2,3 这样列举保留字段编号
-
可以 reserved 2 to 10 这样保留一批字段编号
-
想保留名称也是可以的,例如 reserved "age",如果有多个就用逗号分隔
前后兼容性
根据上述的例子可以得出,pb是具有向前兼容的。为了叙述方便,把增加了“生日”属性的 service称为“新模块”,未做变动的 client 称为“老模块”
-
向前兼容:老模块能够正确识别新模块生成或发出的协议,这时新增加的“生日”属性会被当作未知字段(pb 3.5版本及之后)
-
向后兼容:新模块也能够正确识别老模块生成或发出的协议
前后兼容的作用:
当我们维护一个很庞大的分布式系统时,由于你无法同时升级所有模块,为了保证在升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的“向后兼容”或“向前兼容”
未知字段
未知字段:解析结构良好的 protocolbuffer 已序列化数据中的未识别字段的表示方式
例如:
刚开始的字段名称是age,字段编号是2,新增几个联系人后,将这个字段名称和字段编号删除了,使用 reserved 保留
此时新的联系人信息就没有 age 了,但是序列化后的二进制文件中还会存在原来的 age 信息,此时 age 字段就会被存放在 未知字段中
未知字段可以打印出来:
option选项
.proto 文件中可以声明许多选项,使用 option 标注,选项能影响 proto 编译器的某些处理方式
例如:option.proto文件 初始代码为:
编译后,观察生成的 option.pb.h,PeopleInfo类的父类是 Message:
如果在 option.proto文件 新增这一行代码:
此时编译该文件,观察 PeopleInfo类 的父类,变为 MessageLite类了:
通过上述的例子,就可以明白 option选项 能影响 proto 编译器的某些处理方式
选项的完整列表在google/protobuf/descriptor.proto中定义:
message FileOptions {...} // 文件选项 定义在 File0ptions 消息中
message Message0ptions { ... } // 消息类型选项 定义在 Message0ptions 消息中
message FieldOptions { ... } // 消息字段选项 定义在 Fieldoptions 消息中
message Oneofoptions { ... } // oneof字段选项 定义在 0neofoptions 消息中
message EnumOptions{...} // 枚举类型选项 定义在 Enumoptions 消息中
message EnumValue0ptions {...} // 枚举值选项 定义在 EnumValueOptions 消息中
常用选项
optimize_for:
该选项为文件选项,可以设置 protoc编译器的优化级别,分别为
SPEED、CODE_SIZE、LITE_RUNTIME
受该选项影响,设置不同的优化级别,编译.proto 文件后生成的代码内容不同
-
SPEED:protoc编译器将生成的代码是高度优化的,代码运行效率高,但是由此生成的代码编译后会占用更多的空间,SPEED 是默认选项
-
CODE_SIZE:proto 编译器将生成最少的类,会占用更少的空间,是依赖基于反射的代码来实现序列化、反序列化和各种其他操作。但和 SPEED 恰恰相反,它的代码运行效率较低。这种方式适合用在包含大量的.proto文件,但并不盲目追求速度的应用中
-
LITE_RUNTIME:生成的代码执行效率高,同时生成代码编译后的所占用的空间也是非常少。这是以牺牲Protocol Buffer 提供的反射功能为代价的,仅仅提供 序列化+反序列化 功能,所以我们在链接 BP 库时仅需链接 libprotobuf-lite,而非 libprotobuf 这种模式通常用于资源有限的平台,例如移动手机平台中
allow_alias:
允许将相同的常量值分配给不同的枚举常量,用来定义别名。该选项为枚举选项。
表示给 字段编号 为1的 TEL,起一个 LANDLINE 的别名
以上就是 proto3 的语法部分