借助这些新的数据类型,来继续完善前面使用的通讯录demo。
首先见一下枚举类型如何使用,以及编译后生成的.h文件中为我们提供了那些方法:
syntax = "proto3";
enum PhoneType{
MP = 0; //移动电话
TEL = 1; //固定电话
}
message Phone {
string number = 1;
PhoneType type = 2;
}
Phone这个消息体内包括电话号码,以及电话的类型(座机还是手机)。
看看编译后的文件中为我们提供了那些关于枚举类型的方法:
protoc --cpp_out=./ test_enum.proto //使用该指令编译.proto文件
PhoneType_Name();函数作用是将枚举常量转化为,对应的枚举值的名称。
(例如:0 -> MP)
使用枚举时的规则:
0值常量必须存在且必须为第一个。
枚举定义可以定义在消息体外侧,也可以定义在消息体内。(嵌套定义)
枚举常量取值范围在32位整数范围内,不建议使用负数。(负值无效,与编码规则有关)
同级的枚举常量中,各个枚举类型中常量的名称不能重名。
//在同一proto文件下,会编译报错
enum PhoneType{
MP = 0; //移动电话
TEL = 1; //固定电话
}
enum Test {
TEL = 0;
}
在同一proto文件中,外层的枚举类型和嵌套定义在消息体内的枚举类型不算同级。
enum PhoneType{
MP = 0; //移动电话
TEL = 1; //固定电话
}
message Phone {
string number = 1;
PhoneType type = 2;
enum Test {
TEL = 0;
}
}
引入其他proto文件时,若两个proto文件都没有声明package,且两个枚举类型都在最外侧,算同级处理。
//test_enum2.proto
syntax = "proto3";
enum Test {
TEL = 0;
}
//test_enum.proto
syntax = "proto3";
import "test_enum2.proto";
enum PhoneType{
MP = 0; //移动电话
TEL = 1; //固定电话
}
message Phone {
string number = 1;
PhoneType type = 2;
}
同样会编译报错。
若引入其他proto文件时,枚举类型定义在package内部,那么两个枚举类型不算同级。
//test_enum2.proto
syntax = "proto3";
package test2;
enum Test {
TEL = 0;
}
加上package后就不会编译报错。
字段声明为Any类型,可以理解为泛型类型。在使用时。Any中可以存储任意类型的消息类型。同时Any类型可以用repeated修饰。
Any类型是google已经帮我们定义好的类型,可以在protobuf的安装路径下的include路ing下查看。
在使用时引入 “google/protobuf/any.proto”文件即可。并且使用时要指明其命名空间。
syntax = "proto3";
import "google/protobuf/any.proto";
message Test {
string name = 1;
google.protobuf.Any msg = 2;
}
看下编译后生成了哪些它用于操作Any类型的方法。
mutable函数返回的是指向该类型的指针变量,这类方法以为我们开辟好空间,我们可以通过指针来对这块空间操作。
Any类型可以存储任意类型的消息,这就要设计任意类型和Any类型之间的转换,any.pb.h已经为我们实现了这些方法。
PackFrom方法是将别的消息类型转化为Any类型,UnpackTo方法是将Any类型转化为特定的消息类型。
any类型中还带了一个Is函数,其作用是判断any中存储的消息类型是否与模板类型一致。
syntax = "proto3";
message PeopleInfo {
string name = 1;
string phone_number = 2;
oneof other_contact {
string qq = 3;
string wechat = 4;
}
}
oneof 顾名思义:多选一,如果消息中声明了很多字段,但最终只会有一个字段会被设置,那么就可以用oneof来完成这个效果。上面的other_contact只能被设置为qq 和 微信中的一个。
对于qq,wechat除了提供get/set方法外,还提供了一个_case();的接口,
首先,编译器会将oneof中的多个字段名定义为枚举类型,_case函数返回的就是一个枚举类型表示设置了哪一个字段。
注意:
如果对oneof字段设置了多次,那么只会保留最后一次设置的字段。
protobuf中也支持map这种类型,注意的是map中key-type可以是除了float和bytes类型之外的所有标量类型。value-type可以是任意类型。
注意:
map类型不能被repeated修饰。并且插入到map中的数据是无序的。
syntax = "proto3";
message PeopleInfo {
string name = 1;
map<string,string> remark = 2;
}
map类型的set放法也是返回一块已开辟的空间的地址,用户通过指针去操控这块空间。
contacts.proto
syntax = "proto3";
package contacts;
import "google/protobuf/any.proto";
message Address {
string home_address = 1;
string uint_address = 2;
}
message PeopleInfo {
string name = 1; //姓名
int32 age = 2; //年龄
enum PhoneType {
MP = 0;
TEL = 1;
}
message Phone {
string number = 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 Contact {
repeated PeopleInfo people = 1;
}
write.cc
#include
#include
#include
#include "contacts.pb.h"
using namespace std;
void AddPerson(contacts::PeopleInfo* p) {
cout << "------------添加联系人------------" << endl;
cout << "------联系人姓名: ";
string name;
getline(cin,name);
p->set_name(name);
cout << "------联系人年龄: ";
int age;
cin >> age;
cin.ignore(256,'\n'); //清除缓冲区内的'\n'
p->set_age(age);
for (int i = 0; ; i++) {
cout << "联系人第" << i + 1 << "个电话(只输入空格结束): ";
string number;
getline(cin,number);
if (number.empty()) {
break;
}
contacts::PeopleInfo_Phone* phone = p->add_phone();
phone->set_number(number);
cout << "请输入电话类型(1.移动电话 2.固定电话): ";
int type;
cin >> type;
cin.ignore(256,'\n');
switch (type) {
case 1 :
phone->set_type(contacts::PeopleInfo_PhoneType::PeopleInfo_PhoneType_MP);
break;
case 2 :
phone->set_type(contacts::PeopleInfo_PhoneType::PeopleInfo_PhoneType_TEL);
break;
default:
cout << "输入有误!!!" << endl;
break;
}
}
contacts::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_uint_address(unit_address);
google::protobuf::Any* data = p->mutable_data();
data->PackFrom(address);
cout << "请输入其他联系方式(1.qq 2.微信): ";
int other;
cin >> other;
cin.ignore(256,'\n');
switch (other) {
case 1 :
{
string qq;
cout << "------用户qq号码: ";
getline(cin,qq);
p->set_qq(qq);
break;
}
case 2 :
{
string wechat;
cout << "------用户微信号码: ";
getline(cin,wechat);
p->set_wechat(wechat);
break;
}
default:
cout << "输入有误!!!" << endl;
break;
}
google::protobuf::Map< std::string, std::string >* map = p->mutable_remark();
for (int i = 0; ; i++) {
cout << "请输入第" << i + 1 << "个备注标题(输入空格结束): ";
string key;
getline(cin,key);
if (key.empty()) {
break;
}
string value;
cout << "请输入第" << i + 1 << "个备注内容: ";
getline(cin,key);
map->insert({key,value});
}
cout << "-----------成功添加联系人-----------" << endl;
}
int main() {
contacts::Contact contact;
//1.打开通讯录文件将文件的内容反序列化到内存中
fstream input("./contacts.bin",ios::in | ios::binary);
if (!input) {
cout << "contacts.bin not exist,create it!!!" << endl;
} else if (!contact.ParseFromIstream(&input)) {
cout << "prase error" << endl;
input.close();
return -1;
}
fstream output("./contacts.bin",ios::out | ios::binary | ios::trunc);
AddPerson(contact.add_people());
//序列化写入文件
if (!contact.SerializeToOstream(&output)) {
cout << "Serialize error" << endl;
input.close();
output.close();
return -1;
}
cout << "write sucess!!!" << endl;
input.close();
output.close();
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
read.cc
#include
#include
#include
#include "contacts.pb.h"
using namespace std;
void ReadContact(const contacts::Contact& contact) {
for (int i = 0; i < contact.people_size(); i++) {
const ::contacts::PeopleInfo& person = contact.people(i);
cout << "------联系人" << i + 1 << "------" << endl;
cout << "姓名: " << person.name() << endl;
cout << "年龄: " << person.age() << endl;
//电话信息
for (int j = 0; j < person.phone_size(); j++) {
const ::contacts::PeopleInfo_Phone& phone = person.phone(j);
cout << "第" << j + 1 << "电话号码: " << phone.number()
<< " [" << contacts::PeopleInfo::PhoneType_Name(phone.type()) << "]" << endl;
}
if (person.has_data() && person.data().Is<contacts::Address>()) {
contacts::Address address;
person.data().UnpackTo(&address);
cout << "家庭住址: " << address.home_address() << endl;
cout << "工作地址: " << address.uint_address() << endl;
}
switch (person.other_contact_case()) {
case contacts::PeopleInfo::kQq :
cout << "qq号码: " << person.qq() << endl;
break;
case contacts::PeopleInfo::kWechat :
cout << "微信号码: " << person.wechat() << endl;
default:
break;
}
if (person.remark().size()) {
auto it = person.remark().begin();
while (it != person.remark().end()) {
cout << "备注信息: " << endl
<< " " << it->first << ":" << it->second << endl;
++it;
}
}
}
}
int main (){
fstream input("contacts.bin",ios::in | ios::binary);
//1.从文件中反序列化读取通讯录信息
contacts::Contact contact;
if (!contact.ParseFromIstream(&input)) {
cout << "prase from file error" << endl;
}
ReadContact(contact);
return 0;
}
反序列化消息时,如果被反序列化的二进制序列中不包含某个字段,反序列化对象中有相应字段,该字段就会被设置成该字段对应的默认值。不同类型对应的默认值是不同的:
假设通讯录中只有三个字段,姓名,年龄和电话。在写端将年龄字段修改为了生日,但是在读端并没有做出相应的修改,在添加新的联系人时会发生什么现象呢?
//修改之前的proto文件,读端和写段是一致的
message PeopleInfo{
string name = 1;
int32 age = 2;
string Phone = 3;
}
message Contact{
repeated PeopleInfo people = 1;
}
//修改之后,写端的proto文件做了以下修改
message PeopleInfo{
string name = 1;
int32 birthday = 2; //修改项
string Phone = 3;
}
message Contact{
repeated PeopleInfo people = 1;
}
很显然出了一个问题,写端的生日字段在反序列化的时候被解释成了年龄字段,这是因为我们并没有遵循更新消息的规则,而导致的问题。
更新消息时注意以下几点:
如果通过 删除 或 注释掉 字段来更新消息类型,未来的⽤⼾在添加新字段时,有可能会使⽤以前已经存在,但已经被删除或注释掉的字段编号。将来使⽤该 .proto 的旧版本时的程序会引发很多问题:数据损坏、隐私错误等等。
为了防止这一现象发生,可以使用reserved关键字,对之前的字段编号或者名称进行保留,当我们再使用这些编号或者名称的时候,编译器会报错。
message PeopleInfo{
reserved 2 , 7, 100 to 200; //可以一次保留多个字段编号用逗号隔开
reserved "age"; //也可以像 100 to 200这样使用,表示保留的100到200的字段编号
string name = 1; //也可以一次保留多个字段名称用逗号隔开即可。
//int32 age = 2;
int32 birthday = 4;
string Phone = 3;
}
在上面我们更新了写段的proto文件,对于读端的proto文件没有修改,并且我们对旧字段的名称和编号做了reserved处理,这样就意味着序列化的二进制文件中存在生日这个字段,但是在读端反序列化后的对象中不存在生日的字段,这时候这个生日字段就会被当作未知字段来处理。
获取未知字段的流程:GetReflection();—>GetUnknownfields();---->field();
UnknowField类中,提供了获取未知字段的字段编号,字段的值,字段类型等相关方法。并且对字段类型进行了划分也就是上图中的枚举类型。
对于上面使用reserved保留字段的名称和字段编号后,再次添加联系人的时候,在由于读端仍旧是老版本的proto文件,那么对于生日字段对于其就是未知字段。
const google::protobuf::Reflection *reflection = person.GetReflection();
const google::protobuf::UnknownFieldSet &set = reflection->GetUnknownFields(person);
for (int j = 0; j < set.field_count(); j++)
{
const google::protobuf::UnknownField &f = set.field(j);
cout << "未知字段: " << j + 1 << " :" << endl;
cout << "字段编号: " << f.number()
<< "类型: " << f.type();
switch (f.type())
{
case google::protobuf::UnknownField::Type::TYPE_VARINT:
cout << " 值: " << f.varint() << endl;
break;
case google::protobuf::UnknownField::Type::TYPE_LENGTH_DELIMITED:
cout << " 值: " << f.length_delimited() << endl;
break;
// case ...
}
}
.proto ⽂件中可以声明许多选项,使⽤ option 标注。选项能影响 proto 编译器的某些处理⽅式。选项的完整列表在google/protobuf/descriptor.proto中定义。
option optimize_for = LITE_RUNTIME;
• optimize_for : 该选项为⽂件选项,可以设置 protoc 编译器的优化级别,分别为 SPEED 、CODE_SIZE 、 LITE_RUNTIME 。受该选项影响,设置不同的优化级别,编译 .proto ⽂件后生成的代码内容不同。
◦ SPEED : protoc 编译器将⽣成的代码是⾼度优化的,代码运⾏效率⾼,但是由此⽣成的代码编译后会占⽤更多的空间。 SPEED 是默认选项。
◦ CODE_SIZE : proto 编译器将⽣成最少的类,会占⽤更少的空间,是依赖基于反射的代码来实现序列化、反序列化和各种其他操作。但和 SPEED 恰恰相反,它的代码运⾏效率较低。这种⽅式适合⽤在包含⼤量的.proto⽂件,但并不盲⽬追求速度的应⽤中。
◦ LITE_RUNTIME : ⽣成的代码执⾏效率⾼,同时⽣成代码编译后的所占⽤的空间也是⾮常少。这是以牺牲Protocol Buffer提供的反射功能为代价的,仅仅提供 encoding+序列化 功能,所以我们在链接 BP 库时仅需链接libprotobuf-lite,⽽⾮libprotobuf。这种模式通常⽤于资源有限的平台,例如移动⼿机平台中。
enum PhoneType {
option allow_alias = true;
MP = 0;
TEL = 1;
LANDLINE = 1; // 若不加 option allow_alias = true; 这⼀⾏会编译报错
}
allow_alias : 允许将相同的常量值分配给不同的枚举常量,⽤来定义别名。该选项为枚举选项。