protobuf特殊数据类型
- 一,特殊数据类型
- 1,enum
- 使用规则
- 注意事项
- 2,Any
- 3,oneof
- 4,map
- 通讯录demo升级
- 二,默认值
- 三,更新消息
- 更新规则
- 保留字段reserved
- 未知字段
- 获取未知字段
- 四,选项option
- 常见选项
一,特殊数据类型
🚀借助这些新的数据类型,来继续完善前面使用的通讯录demo。
1,enum
使用规则
🚀首先见一下枚举类型如何使用,以及编译后生成的.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后就不会编译报错。
2,Any
🚀字段声明为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中存储的消息类型是否与模板类型一致。
3,oneof
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字段设置了多次,那么只会保留最后一次设置的字段。
4,map
🚀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放法也是返回一块已开辟的空间的地址,用户通过指针去操控这块空间。
通讯录demo升级
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 <iostream>
#include <fstream>
#include <string>
#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 <iostream>
#include <fstream>
#include <string>
#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;
}
二,默认值
🚀反序列化消息时,如果被反序列化的二进制序列中不包含某个字段,反序列化对象中有相应字段,该字段就会被设置成该字段对应的默认值。不同类型对应的默认值是不同的:
- 对于字符串,默认值为空字符串。
- 对于字节,默认值为空字节。
- 对于布尔值,默认值为false。
- 对于数值类型,默认值为0。(浮点型就是0.0)
- 对于枚举,默认值是第一个枚举常量,必须为0。
- 对于消息字段,未设置该字段。它的取值依赖于语言。
- 对于设置了repeated的字段,默认值是空的。
- 对于消息类型,oneof字段,和any字段,C++和Java语言中都有has_方法来检测当前字段是否被设置。
三,更新消息
🚀假设通讯录中只有三个字段,姓名,年龄和电话。在写端将年龄字段修改为了生日,但是在读端并没有做出相应的修改,在添加新的联系人时会发生什么现象呢?
//修改之前的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;
}
🚀很显然出了一个问题,写端的生日字段在反序列化的时候被解释成了年龄字段,这是因为我们并没有遵循更新消息的规则,而导致的问题。
更新规则
🚀更新消息时注意以下几点:
- 禁⽌修改任何已有字段的字段编号。
- 若是移除⽼字段,要保证不再使⽤移除字段的字段编号。正确的做法是保留字段编号(reserved),以确保该编号将不能被重复使⽤。不建议直接删除或注释掉字段。
- int32, uint32, int64, uint64 和 bool 是完全兼容的。可以从这些类型中的⼀个改为另⼀个,⽽不破坏前后兼容性。若解析出来的数值与相应的类型不匹配,会采⽤与 C++ ⼀致的处理⽅案(例如,若将 64 位整数当做 32 位进⾏读取,它将被截断为 32 位)。
- sint32 和 sint64 相互兼容但不与其他的整型兼容。
- string 和 bytes 在合法 UTF-8 字节前提下也是兼容的。
- bytes 包含消息编码版本的情况下,嵌套消息与 bytes 也是兼容的。
- fixed32 与 sfixed32 兼容, fixed64 与 sfixed64兼容。
- enum 与 int32,uint32, int64 和 uint64 兼容(注意若值不匹配会被截断)。但要注意当反序列化消息时会根据语⾔采⽤不同的处理⽅案:例如,未识别的 proto3 枚举类型会被保存在消息中,但是当消息反序列化时如何表⽰是依赖于编程语⾔的。整型字段总是会保持其的值。
- oneof:
◦ 将⼀个单独的值更改为 新 oneof 类型成员之⼀是安全和⼆进制兼容的。
◦ 若确定没有代码⼀次性设置多个值那么将多个字段移⼊⼀个新 oneof 类型也是可⾏的。
◦ 将任何字段移⼊已存在的 oneof 类型是不安全的。
保留字段reserved
🚀如果通过 删除 或 注释掉 字段来更新消息类型,未来的⽤⼾在添加新字段时,有可能会使⽤以前已经存在,但已经被删除或注释掉的字段编号。将来使⽤该 .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处理,这样就意味着序列化的二进制文件中存在生日这个字段,但是在读端反序列化后的对象中不存在生日的字段,这时候这个生日字段就会被当作未知字段来处理。
- 未知字段:解析结构良好的 protocol buffer 已序列化数据中的未识别字段的表⽰⽅式。例如,当旧程序解析带有新字段的数据时,这些新字段就会成为旧程序的未知字段。
- 本来,proto3 在解析消息时总是会丢弃未知字段,但在 3.5 版本中重新引⼊了对未知字段的保留机制。所以在 3.5 或更⾼版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果中。
获取未知字段
🚀获取未知字段的流程: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 ...}
}
四,选项option
🚀.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 : 允许将相同的常量值分配给不同的枚举常量,⽤来定义别名。该选项为枚举选项。