ProtoBuf使用特点
ProtoBuf是需要依赖通过编译生成的头文件和源文件来使用的
class XXX{
定义一系列属性字段
处理字段的方法: get set
处理类的方法:序列化、反序列化(将结构化的数据与二进制字节序列之间进行相互转化)
}
在ProtoBuf中,这不叫做类,叫做message(.proto文件),只需要定义一系列属性字段,经过PB编译器,生成上面的代码
.proto文件结构
- 首行:语法指定行,没有指定,就默认使用proto2语法
- package定义在.proto文件内的声明空间
- 定义联系人message,建议使用大驼峰;字段编号必须存在,因为编译原理。
- 字段编号不能够使用19000~19999,这个范围的数字是被ProtoBur所保留的。
- int32和sint32都是变长编码,但是对于负数编码建议采取sint32;fixed32和fixed64是定长编码
- 对于将会经常被使用的字段,建议将其字段编号设置为1~15,因为这个区间的数字序列化之后只有一个字节。
syntax = "proto3";
package contacts;
message PeopleInfo{
string name = 1;
int32 age = 2;
}
.proto文件的编译
编译命令:protoc -I path/to/proto --cpp_out=target_dir name.proto其中protoc为基本编译指令,-I用来指定编译.proto文件的搜索路径,-cpp_out表示生成的C++代码的存放位置。
随后,将在我们的指定位置生成分别存放方法声明和实现的头源文件。其中有各种get、set方法,至于序列化和反序列化,是继承自父类MessageLite。
序列化的函数均为const函数,并不会将类中的内容进行修改,只是将序列化之后的内容保存到函数指定的位置
.proto编译出的结果是二进制序列,所以破解成本增大,但是也意味着相对安全
编译使用protobuf的代码:g++ -o TestPb main.cc contacts.pb.cc -std=c++11 -l protobuf
要指定生成PB编译器生成的源文件,同时还要指定c++标准为c++11,因为PB编译器生成的源文件含有C++11语法,同时还需要指定protobuf链接库,否则会有语法错误.
proto3语法
字段规则与消息定义.
写入
在定义message时,可以嵌套定义(在一个message中定义另一个message),也可以在一个.proto文件中定义多个message,同时,我们也可以引入在另一个.proto文件中定义的message(需要注意的是,通过impout引入之后,还需要在定义字段时,加上引入文件的package的名称作为使用前缀)
对于嵌套定义的message、enum等,在访问时采取BaseName_Targetname的方式访问。
例如下面的例子中,就是contacts2.PeopleInfo_Address
//通讯录.proto文件
syntax="proto3";
package contacts2;
import "school.proto";
message Phone{
string number = 1;
}
message PeopleInfo{
string name = 1;
int32 age = 2;
message Address{
string number = 1;
}
Address address = 3;
repeated Phone phone = 4;
school.School school = 5;
}
message Contacts{
repeated PeopleInfo contacts = 1;
}
//用来引入的school.proto文件
syntax="proto3";
package school;
message School{
string name = 1;
}
repeated关键字用来创建一个数组。插入的时候,对于非repeated修饰的字段,直接通过set_typename(val)插入即可,对于repeated的字段,通过set_typename()获得一个指向数组的指针,在通过往这个指针中调用set_typename(val)的方式插入(如果其中依然有数组的话,那么继续通过获取指针的方式插入)
//向一个通讯录中插入姓名、年龄、电话的演示(文件)
#include <iostream>
#include <fstream>
#include "./pb_files/contacts.pb.h"
void AddContacts(contacts2::PeopleInfo* people)
{
std::cout << "----------新增联系人----------" << std::endl;
std::string name;
std::cout << "请输入姓名:";
std::getline(std::cin, name);
people->set_name(name);
int age;
std::cout << "请输入年龄:";
std::cin >> age;
people->set_age(age);
std::cin.ignore(256, '\n');
for(int i = 1;; i++){
std::cout << "请输入电话" << i << ":";
std::string number;
std::getline(std::cin, number);
if(number.empty()){
break;
}
contacts2::Phone* phone = people->add_phone();
phone->set_number(number);
}
std::cout << "----------新增成功----------" << std::endl;
}
int main(){
contacts2::Contacts contacts;
std::ifstream input("contacts.bin", std::ios::in | std::ios::binary);
if(!input){
std::cout << "----------file not exists create one----------" << std::endl;
}
else if(!contacts.ParseFromIstream(&input)){
std::cout << "Parse from file fail" << std::endl;
return -1;
}
std::cout << "----------Parse success----------" << std::endl;
AddContacts(contacts.add_contacts());
std::ofstream write("contacts.bin", std::ios::out | std::ios::trunc | std::ios::binary);
if(!contacts.SerializeToOstream(&write)){
std::cout << "write fail" << std::endl;
return -1;
}
std::cout << "----------write success----------" << std::endl;
return 0;
}
读取
对于数组的读取,也是获得相应元素的指针,然后从中使用get方法读取,不过set函数采取的是下划线加成员变量名的方式,但是get方法省略的get,直接使用成员名即可
#include <iostream>
#include <fstream>
#include "./pb_files/contacts.pb.h"
void PrintContacts(contacts2::Contacts& contacts){
for(int i = 0; i < contacts.contacts_size(); i++){
std::cout << "----------联系人信息" << i + 1 << "----------" << std::endl;
contacts2::PeopleInfo people = contacts.contacts(i);
std::cout << "姓名:" << people.name() << std::endl;;
std::cout << "年龄" << people.age() << std::endl;
for(int j = 0; j < people.phone_size(); j++){
contacts2::Phone phone = people.phone(i);
std::cout << "联系人电话" << j + 1 << phone.number() << std::endl;
}
}
}
int main(){
contacts2::Contacts contacts;
std::fstream input("contacts.bin", std::ios::in | std::ios::binary);
if(!contacts.ParseFromIstream(&input)){
std::cout << "Parse from contacts.bin fail" << std::endl;
input.close();
return -1;
}
PrintContacts(contacts);
return 0;
}
decode指令
可以快速查看二进制文件中的内容,默认是从标准输入中读取文件,但是可以通过输入重定向的方式来获取二进制文件的内容。
wangjiale@ubuntu:~/protobuf/proto3$ protoc --decode=contacts2.Contacts contacts.proto < contacts.bin
其中decode指定要查看那个package中的那个message,再指定要看哪个.proto文件,最后输入重定向
枚举类型
在.proto文件中使用enum关键字,字段编号必须从0开始,且字段编号为0的部分将被作为枚举默认值。在同级枚举中,不可存在相同名称的枚举值(对于引入的外部.proto文件,如果未指定package名称,则也算统计枚举,不可重名)
获取枚举值的名称(不是数字,采用EnumName_Name(target)的方法)
Any类型
是一个泛型,可以转化为任意其他类型。
使用时需要引入google/protobuf/any.proto
需要使用它时,首先需要先实例化一个目标message,然后将目标父指针中的Any类型通过mutable_name()获取(_mutable字段用来获取一个可变引用),获取到之后通过PackFrom()来将目标类型转化为Any类型存储。
读取时,通过UnPackTo来读取到指定message中进行处理
import "google/protobuf/any.proto";
message PeopleInfo{
string name = 1;
int32 age = 2;
repeated Phone phone = 3;
google.protobuf.Any data = 4;
}
//写入
contacts2::Address address;
people->mutable_data()->PackFrom(address);
//读取
if(people.has_data() && people.data().Is<contacts2::Address>()){
contacts2::Address address;
people.data().UnpackTo(&address);
}
oneof
- 设定的值属于同一个message中其他元素的同一级,所以字段编号、名称不能重复。设置时也直接采用set_name的方式即可
- oneof不能设定repeated修饰,oneof中的内容,都通过枚举类型的方式存储,但是多了一个not known字段。
- 读取的时候,要获取时,通过oneof_name_case方式获取,如果需要获得int编号,可以通过OneofNameCase()方式获取
- 枚举值的命名,通过kName的方式生成。
map
map类型通过键值对的方式存储,key值可以是除了float和byte以外的任意标量类型,value可以是任意类型;与Any一样不能用repeated修饰
插入数据时,通过mutable_字段函数即可获取可改变的对象指针,然后通过insert构造pair插入即可;读取时,使用_size字段结合迭代器即可完成遍历,通过first获取key,second获取value
字段的删除
字段是不建议直接删除的,如果删除,要保证该字段编号不再被其他字段使用,因为PB编译器是根据字段编号来进行值的存储分配的,如果继续使用,会导致可能会字段内容的不一致。解决方法是使用reserved关键字,使用reserved修饰的字段,会被pb编译器警告该字段不可用。
未知字段
解析不出的字段在3.5版本恢复之后会被保留在未知字段。
通过MessageList中的GetReflection来获得指向google::protobuf::Reflection的指针,通过其中的GetUnknownFields(const Message& message),获得UnknownFidlsSet,遍历获取所有的未知字段。
未知字段信息的获取,首先通过number获取字段编号,通过type获取类型的枚举值,在通过对应的类型名称的方法就可以获取未知字段的值。
(比如TYPE_TARINT表示未知字段是int32或者int64类型,可以通过varint来获取具体值)
前后兼容性
一般来说,只要不破坏字段规则是不会破坏前后兼容性的;
向前兼容是指老模块能够正确识别新模块生成或发出的协议,在3.5之后可以做到,不可识别的字段会被放在未知字段当中;向后兼容是指新模块能够正确识别老模块发出或生成的协议。
option选项
optimize_for设置pb编译器优化级别
- SPEED高度优化,默认选项,速度更快,但是大小更大
- CODE_SIZE,生成最少的类,占用空间更小,但是运行效率更低
- LITE_RUNTIME,效率高,空间也小,代价是牺牲了反射能力(比如说不再继承Message而是直接继承子MessageLite,会导致不能够使用Message的方法比如reflection,只能够使用序列化、反序列化方法)。
allow_alias=true允许别名。
允许别名之后,同一个字段编号可以设置相同名称
6093

被折叠的 条评论
为什么被折叠?



