目录
一.初识ProtoBuf
1.序列化和反序列化的概念
序列化: 就是将一个对象转换为字节序的过程;
反序列化 就是将一个字节序转换为一个完整对象的过程;
什么时候需要序列化:
存储数据:当你想把的内存中的对象状态保存到⼀个⽂件中或者存到数据库中时。
⽹络传输:⽹络直接传输数据,但是⽆法直接传输对象,所以要在传输前序列化,传输完成后反 序列化成对象。例如我们之前学习过socket编程中发送与接收数据。
如何实现序列化?
主流的序列化和反序列化工具有:XML、JSON、ProtoBuf;
2.什么是ProtoBuf
Protocol Buffers是Google的⼀种语⾔⽆关、平台⽆关、可扩展的序列化结构数据的⽅法,它可⽤于(数据)通信协议、数据存储等。
Protocol Buffers类⽐于XML,是⼀种灵活,⾼效,⾃动化机制的结构数据序列化⽅法,但是⽐XML更⼩、更快、更为简单。
你可以定义数据的结构,然后使⽤特殊⽣成的源代码轻松的在各种数据流中使⽤各种语⾔进⾏编写和读取结构数据。你甚⾄可以更新数据结构,⽽不破坏由旧数据结构编译的已部署程序
总结:
- 语言无关、平台无关: 即ProtoBuf支持Java、C++、Python等多种主流语言,支持Window、Linux等多个平台
- 高效: 即比XML更小、更快、更为简单。
- 扩展性好、兼容性好: 你可以更新数据结构,而不影响和破环原有的旧程序;
3.ProtoBuf的工作特点
假设在没有ProtoBuf序列化和反序列化工具之前,我们定义了一个类,然后想要对这个类进行序列化和反序列化工作,那么就需要我们自己在该类中设计和构造一个序列化和反序列化的方法,这样的话对于我们的开发效率会有影响。
当我们引入ProtoBuf工具过后,我们就只需要在 .proto文件中定义我们的类即可,不需要为这个类设计任何方法,再使用ProtoBuf的编译器来编译这个.proto文件,就会得到一个.cpp和.hpp的文件,这两个文件中分别放着我们当初在.proto文件中定义的那个类的C++的实现和声明,并且ProtoBuf的编译器在编译的过程中会为我们在.proto文件中定义的那个类自动生成序列化和反序列化方法以及一些与属性字段相关的方法,我们只需要在我们的工作代码中包含以下ProtoBuf编译出来的.cpp和.hpp文件就可以了,在我们的工作代码中就能直接使用,我们定义的类的序列化和反序列化的方法了;
从而省去了我们程序员自己开发一个类的序列化和反序列化的工作;
使用方法:
1.编写.proto⽂件,⽬的是为了定义结构对象(message)及属性内容。
2. 使⽤ protoc 编译器编译 .proto ⽂件,⽣成⼀系列接口代码,存放在新⽣成头⽂件和源⽂件中。
3. 依赖⽣成的接口。将编译⽣成的头⽂件包含进我们的代码中,使用编译器为我们生成的序列化,反序列化方法以及一些对消息字段进行读写的方法。
因此:
ProtoBuf是需要依赖通过编译⽣成的头⽂件和源⽂件来使用的
二.学习思路
对ProtoBuf的完整学习,将使⽤项⽬推进的⽅式完成学习:即对于ProtoBuf知识内容的展开,会对⼀个项⽬进⾏⼀个版本⼀个版本的升级去讲解ProtoBuf对应的知识点.
在后续的内容中,将会实现⼀个通讯录项⽬。对通讯录⼤家应该都不陌⽣,⼀般,通讯录中包含了⼀批的联系⼈,每个联系⼈⼜会有很多的属性,例如姓名、电话等等。
随着对通讯录项⽬的升级,我们对ProtoBuf的学习与使⽤就越深⼊。
三.快速上手
在快速上⼿中,会编写第⼀版本的通讯录1.0。在通讯录1.0版本中,将实现:
• 对⼀个联系⼈的信息使⽤ProtoBuf进⾏序列化,并将结果打印出来.
• 对序列化后的内容使⽤ProtoBuf进⾏反序列,解析出联系⼈信息并打印出来。
• 联系⼈包含以下信息:姓名、年龄。
通过通讯录1.0,我们便能了解使⽤ProtoBuf初步要掌握的内容,以及体验到ProtoBuf的完整使⽤流程。
我们将联系人信息类先定义在.proto
文件中,然后利用protoc编译器编译这个文件,然后我们在自己的工作代码中引用这个编译出来的类就好。联系人信息类在经过protoc编译过后会自动变为含有序列化和反序列化方法的C++类,我们只需要直接调用现成的方法即可。
步骤如下:
1. 创建一个.proto
文件:
这里我们创建一个PeopleInfo.proto文件,该文件用来定义联系人信息类;
2. 我们需要在这个PeopleInfo.proto
文件中完成一些初始化工作。比如:指定PB语法版本、为当前.proto
文件中的数据指定作用域;
① 我们可以利用syntax
关键字来指定当前.proto
文件锁采用的语法版本
注意这个语法版本声明需要放在当前.proto文件的首行(注释不算一行),其次就是我们一般都是需要指定当前.proto文件所采用的语法版本为"proto3"的版本,如果不指定的话当前.proto文件采用proto2语法进行编译,proto2语法相对于proto3语法在编程语言支持上没有proto3广,同时对于一些语法的支持也不是很好,为此实际开发中我们通常采用proto3语法来编译当前文件。
② 我们也可以为当前文件中定义的类声明一个命名空间,来避免不同 .proto之间的命名冲突的问题;
这一点我们可以利用关键字package来实现:
当然,这个命名空间的定义并不是强制的,但是为了养成良好的编程习惯,我们还是希望能将其定义出来;package 定义的命名空间在经过protoc编译器编译过后会变为C++中的namespace 命名空间,其中包含着在当前.proto文件中定义的各种类;
3. 初始化工作都完成的差不多了,我们就可以开始定义类了,在.proto
文件中定义类是利用message
关键字来完成的。
在message 定义的类中,我们只需要定义出该类所包含的属性即可,同时我们需要给这些属性进行编号,并且每个同级属性之间不能重复,这是PB语法要求的;
因此在message 中定义的字段格式如下:
同时编号也不是随便乱设的,编号也是有范围的,编号只能从1 ~ 2^29 -1之间进行取数,其中19000 ~ 19999不可取,因为这些编号已经被PB官方征用了!使用了编译就会出错;
在字段命名上,我们也是要注意一点规范,比如使用全小写字母、多个字母之间使用下划线连接;
字段类型可以分为:标量数据类型和特殊数据类型(eg:枚举、其它消息类型);
接下来,我们可以看一看PB中那些标量类型,与C++中的那些类型相对应:
.proto TYPE NOTES C++ TYPE double null double float null float int32 使用变成编码。负数的编码效率比较低(若字段出现负数的频率比较高,可以考虑使用sint32代替) int32 int64 使用变长编码。负数的编码效率比较低(若字段出现负数的频率比较高,可以考虑使用sint64代替) int64 uint32 使用变长编码 uint32 uint64 使用变长编码 uint64 sint32 使用变长编码。符号整型。负数编码效率高于常规int32 int32 sint64 使用变长编码。符号整型。负数编码效率高于常规int64 int64 fixed32 定长4字节。若值常大于2^28则会比uint32更高效 uint32 fixed64 定长8字节。若常值大于2^28则会比uint64更高效 uint64 sfixed32 定长4字节 int32 sfixed64 定长8字节 int64 bool null bool string 包含UTF-8和ASCII编码的字符串,长度不能超过2^32 string bytes 可以包含任意的字节序列但长度不能超过2^32 string 注意:
1.上表中左边对应的是PB提供的类型,右边表示的是该PB类型在C++语法中对应的C++类型,我们在PB文件中定义类时需要使用PB语法提供的类型来定义类,而不是C++语法提供的类型;
2.上面所说的变长编码就是说,在经过protoc编译过后,原本4字节或8字节的数据可能会被以其更少的字节或更多的字节来进行编码(或序列化);举个例子:就比如我们上述在PB文件中定义的PeopleInfo类中的age字段:
在PB文件中是int32类型,但是在经过proto编译过后在C++中就对应int32类型,也就是4字节的数据,当我们在C++中调用序列化方法来编码该字段时,该序列化方法可能会用低于4字节或高于4字节的数据来进行编码,也就是说序列化方法在序列化字段的时候会判断一下当前字段在PB文件中对应的是什么类型,然后再决定采用变长编码还是定长编码的方式来进行编码;假设现在我们将age在PB文件中的类型换为sfixed32类型,那么在使用protoc编译过后在C++中对应的就是int32类型,那么此时在调用序列化编码该字段的时候就会以4字节的方式来序列化该字段,也就是定长编码的方式来序列化!
对于反序列化也是同理,反序列化方法会自动识别字段类型和编码方式,从而准确的完成反序列化操作,从而将我们的数据正确解码为C++中正确的数据类型!
3.序列化方法在编码的时候除了会将当前字段的数值进行编码外还会对当前字段的PB类型、PB编号进行编码,这么做到目的就是为了方便反序列化的时候能够正确的解码出数据!
4.1 ~ 15范围内的编号需要一个字节进行编码,16 ~ 2047内的数据需要两个字节进行编码。所以1 ~ 15要用来标记出现非常频繁的字段,要为将来有可能添加的、频繁出现的字段预留⼀些出来。
4. 利用protoc编译PeopleInfo.proto文件
编译命令:
protoc [--proto_path=IMPORT_PATH] --cpp_out=DST_DIR path/to/file.proto
含义:
protoc 是 Protocol Buffer 提供的命令⾏编译⼯具。
--proto_path 指定 被编译的.proto⽂件所在⽬录,可多次指定。可简写成 -I IMPORT_PATH 。如不指定该参数,则在当前⽬录进⾏搜索。当某个.proto ⽂件 import 其他 .proto ⽂件时,或需要编译的 .proto ⽂件不在当前⽬录下,这时就要⽤-I来指定搜索目录。
--cpp_out= 指编译后的⽂件为 C++ ⽂件。
DST_DIR 编译后⽣成⽂件的⽬标路径。
path/to/file.proto 要编译的.proto⽂件。
最后编译出如下文件:
编译contacts.proto⽂件后会⽣成什么 编译contacts.proto⽂件后,会⽣成所选择语⾔的代码,我们选择的是C++,所以编译后⽣成了两个文件: contacts.pb.h contacts.pb.cc 。
之后我们只需要在我们的C++代码中包含PeopleInfo.pb.h这个头文件就能在我们的业务代码中使用PeopleInfo类的序列化和反序列化方法。
我们进一步阅读.h文件:
package定义的命名空间会转换为C++中的namespase命名空间
每个字段都有设置和获取的方法,getter的名称与小写字段完全相同,setter方法以set_开头。
每个字段都有⼀个clear_方法,可以将字段重新设置回empty状态
在消息类的⽗类MessageLite 中,提供了读写消息实例的⽅法,包括序列化⽅法和反序列化⽅法。
总结:
对于编译⽣成的C++代码,包含了以下内容:
• 对于每个 message,都会⽣成⼀个对应的消息类。
• 在消息类中,编译器为每个字段提供了获取和设置⽅法,以及⼀下其他能够操作字段的⽅法。
• 编辑器会针对于每个.proto ⽂件生成 .h 和.cc ⽂件,分别⽤来存放类的声明与类的实现。
5.编写mian.cc文件代码
所有准备工作都已经准备好了,接着我们开始着手使用序列化和反序列化方法,我们的目的是将一个联系人的信息进行序列化,然后打印序列化结果,然后再反序列化,打印出反序列化的结果,为此我们的代码可以这样写:
6.编译工作
然后我们采用g++编译器进行编译,编译的时候一定不要忘记链接protobuf库,不然会报连接错误
运行结果:
小结ProtoBuf使⽤流程:
1.编写.proto文件,目的是定义结构对象(message)及属性内容;
2.使用protoc编译器编译.proto文件,将我们在.proto文件中定义的类转换成对应编程语言中的类并且添加各种操作属性字段的方法和序列化、反序列化方法;
3.依赖生成的接口,将编译生成的代码引入我们的工作代码中,实现对于message中定义的字段的获取和设置,同时完成对于message对象的序列化和反序列化;
总的来说:ProtoBuf是需要依赖通过编译⽣成的头⽂件和源⽂件来使⽤的。
四.proto3语法详解
在语法详解部分,依旧使⽤项⽬推进的⽅式完成学习。这个部分会对通讯录进⾏多次升级,使⽤2.x表⽰升级的版本,最终将会升级如下内容:
1.不再打印联系⼈的序列化结果,⽽是将通讯录序列化后并写⼊⽂件中:
2.从⽂件中将通讯录解析出来,并进⾏打印
3.新增联系⼈属性,共包括:姓名、年龄、电话信息、地址、其他联系⽅式、备注
1.字段规则
singular:消息中可以包含该字段零次或⼀次(不超过⼀次)。proto3语法中,字段默认使⽤该规则。
repeated :消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。说大白话就是利用该规则可以定义数组。
eg:
现在我们的PeopleInfo中含有name、age字段,现在我想向其中添加电话号码字段,我们可以在message中定义一个string phone=3;但是一个用户可能不止一个手机号啊,因此我们可以使用一个数组来存储用户的电话号码,因此我们可以将我们的phone改为一个数组类型:repeated string Phones=3;
2.消息类型的定义与使用
在单个.proto文件中可以定义多个message,同时proto3语法也支持嵌套1定义message,并且不限制嵌套深度;
单个.proto文件中可以定义多个message
可嵌套定义message
可导入其他文件中的message
import "info.proto"; //导入定义
2.1代码示例
创建一个通讯录2.0版本对于上面两个语法进行操练;
通讯录2.0版本要求:
- 将联系人信息放入通讯录,然后将通讯录进行序列化,将序列化结果写入文件中;
- 在写一个读程序将序列化结果从通讯录中读取出来,并打印;
- 联系人信息包括,姓名、年龄、电话等字段。
步骤:
1.在一个.proto文件中创建多个message。定义联系人类,通讯录类:
2.使用protoc编译当前的.proto文件使其生成对应的C++文件:
生成:
3.编写代码
为了方便观察结果,我们可以专门写两个程序,一个专门向通讯录进行写入的程序,一个专门从通讯录读取的程序:
write.cc:
read.cc:
makefile:
4.使用make编译程序
运行结果:
我们直接查看contacts.bin:
发现里面存的全是二进制,查看二进制文件,我们可以利用命令:
hexdump -C xxx.bin文件来查看二进制文件;hexdump是Linux下的一个二进制文件查看工具,它可以将二进制文件转换为ASCII、八进制、十进制、十六进制格式进行查看。
-C:将每字节数据以16进制和ASCII码形式展示出来:
3.enum枚举类型
proto3支持我们定义枚举类型并使用,在.proto文件中的写法就是:
enum xxx{
YYY=0;
ZZZ=1;
//...
}
1.与C++中定义的枚举类型差不多,但是这里要注意,在proto文件中定义的枚举类型的第一个常量值必需是0,同时proto也会把enum类型的第一个枚举常量作为enum字段的默认值!
2.枚举类型可以在message外面定义,也可以在message 内部嵌套定义;
3.枚举类型的常量值在32位整数之间,但是负数无效故不建议使用,这与ProtoBuf的编码规则有关;
4.proto文件中的枚举类型不会自增,因此每个枚举类型都需要我们亲自赋值,同时需要以分号(;)结尾;
3.1 enum注事事项
1.同级的枚举类型不能还有相同名称的枚举常量:
2.在同一个.proto文件中,最外层枚举类型和嵌套定义的枚举类型不算同级,即使处于同级但是处于不同作用域下的枚举常量也不算重复定义:
3.多个.proto文件下,若一个文件引入了其它文件,且最外层的枚举类型含有相同的枚举常量,算同级,算重复定义:
3.2 代码示例
在上一版的通讯录中,我们的联系人信息包括了:姓名、年龄、电话信息;
其中电话信息中包括,电话号码和电话号码归属地,现在我们可以使用enum来进行添加电话号码类型:
步骤一:编写.proto文件
步骤二:使用protoc编译器编译
观察.h文件中enum类型伴生出那些方法:
首先,对于枚举类型,protoc为枚举类西编译出了一些全局函数,其中比较常用的就是:
枚举类型名_IsValid(value)
//判断values值是不是枚举常量枚举类型名_Name(enum_value)
//将枚举常量转换为字符串;
步骤三:编写代码
write.cc新增:
read.cc:
步骤四:使用g++编译
运行结果:
问题:我们在添加联系人1和2时,并未添加代表号码运营商的字段,打印时却打印出了中国移动?
实际上这一点,可以用之前的修饰规则来解释,在proto3语法中每个字段默认都是有singular规则修饰的,如果在该字段的反序列化序列中找不到对应字段的编码值,那么该字段会被使用当前类型的默认值来进行填充,而我们的张三的反序列化数据中是没有枚举数据类型的,因此在反序列化的时候,对于张三的枚举字段则是用枚举类型的默认值来填充的,而当前枚举类型的默认值就是ZG_YD,因此我们才看到张三数据在枚举字段显示的是ZG_YD;
4.any类型
Any类型可以理解为一个泛类型,利用Any类型定义的字段可以接收任意类型的值;
Any类型本质上就是ProtoBuf中的一个自定义message,由potobuf官方为我们定好的,因此当我们想要使用该类型的时候,我们需要导入Any.proto:
import "google/protobuf/any.proto";
4.1 代码示例
步骤一:编写.proto文件
步骤二:使用protoc编译器编译
Any类型字段会伴生出那些方法:
步骤三:编写代码
write.cc
read.cc
步骤四:使用g++编译
运行结果:
5.oneof 类型
如果消息中有很多可选字段,但是将来只会有一个字段被设置,那么就可以利用oneof类型加强这一行为,也能有节约内存的效果;
5.1 代码示例
在上一版本中的通讯录中,联系人对象具有:姓名、年龄、电话、地址等信息,现在我们有个新需求,我们需要向联系人中添加一个新字段(其它联系方式字段),该字段可以存储除了电话号码之外其它的联系方式,比如QQ、微信等,但是这个其它联系方式字段只能记录多种社交联系方式中的一种,也就是说,该字段要么保留QQ号要么保留微信号,不能两个一起保存,如果你对其先进行设置了QQ号然后又对其进行设置了微信号,那么该字段会以最后一次设置的值进行保留,反之亦是如此;
为此,我们的PeopleInfo.proto文件可以可以按照如下方式写:
步骤一:编写.proto文件
步骤二:使用protoc编译文件
步骤三:编写代码
write.cc
read.cc
步骤四:使用g++编译读写文件
运行结果:
6.map类型
语法⽀持创建⼀个关联映射字段,也就是可以使⽤ map 类型去声明字段类型,格式为:
map<key_type, value_type> map_field = N;
其中:
- key_type:必须是除了float、bytes外的标量类型;
value_type: 无类型限制- map字段不能使用repeated字段进行修饰
- map存入的元素是无序的;
6.1 代码示例
根据上一版本的通讯录中,联系人信息包括:姓名、年龄、电话信息、地址信息、其它联系方式字段,现在有一个新需求,我们期望给联系人信息中新增备注字段,这一点的话我们可以利用map类型来实现,其中以标题作为map的K值,内容作为map的value值;
因此,我们的PeopleInfo.proto文件可以按照如下方式写:
步骤一:编写.proto文件
步骤二:使用protoc编译文件
步骤三:编写代码
write.cc
read.cc
步骤四:使用g++编译文件
运行结果:
7.ProtoBuf生成的方法的规律
1. 如果是protoBuf的内置标量类型,那么生成常用方法如下:
xxx();//获取字段(const 对象)
set_xxx();//设置字段
clear_xxx();//清除字段
mutable_xxx();//获取字段的地址(诸如string、bytes等类型才会生成,其它内置类型不会);
2. 如果是message自定义类型,那么生成常用方法如下:
xxx();//获取该字段(const 对象)
mutable_xxx();//获取该字段的地址;
clear_xxx();//清理该字段
has_xxx();//判断该字段是否被设置
3. 如果是数组字段,那么会生成常用方法如下:
xxx_size();//获取数组元素个数;
xxx(index);//获取第index个元素;
mutable_xxx(index);//获取第index个元素的下标;
clear_xxx();//清理数组
Add_xxx();//获取插入位置
4. enum枚举字段,生成常用方法:
两个全局方法:
XXX_Name(values);//将枚举常量values变为字符串
XXX_IsValid(values);//判断values是不是枚举常量
常规方法:
clear_xxx();//清理
xxx();//获取
set_xxx();//设置
注意:其中XXX为枚举类型名,xxx为字段名
5. Any字段,生成常用方法:
Any字段内部方法:
PackFrom(mes);//将mes设置给Any字段
UnpackTo(mes);//将Any字段还原成mes对象
template< class T>
bool Is();//判断挡墙Any字段中是不是存的T类型的值;
常规方法:
has_xxx();//Any字段是否被设置
clear_xxx();//清理
xxx();//获取Any字段
mutable_xxx();//获取any字段地址
6. onefo字段,常用方法:
子字段方法:
xxx();
clear_xx();
set_xxx();
mutable_xxx();
has_xxx();
针对oneof字段的方法:
clear_XXX();//清理oneof字段里放的值
XXX_case();//获取oneof字段使用的那个子字段,以枚举类型返回(oneof中的每个子字段都会被protoc编译成一个枚举常量,放在同一个枚举类型中);
注意:其中XXX为oneof类型名,xxx为oneof子字段名
7. map字段,常用方法:
clear_xxx();
xxx_size();
xxx();
mutable_xxx();
8.常用序列化和反序列化方法:
常用序列化方法:
bool SerializeToOstream(ostream * output) const;//将序列化结果放入流里面(标准流、文件流、字符串流);
bool SerializeToArray(void *data, int size) const;//将序列化结果放入字节流里面
bool SerializeToString(string* output) const;//将序列化结果放入字符串里面
常用反序列化方法:
bool ParseFromIstream(istream* input);//从流里面读取反序列化结果;
bool ParseFromArray(const void* data, int size);//从字节流里面读取反序列化结果;
bool ParseFromString(const string& data);//从字符串中读取反序列化结果
8.默认值
对于proto3的语法来说message中的字段默认是用singular来修饰的,被singular修饰的字段在序列化的时候如果没有被设置字段值,那么protobuf的序列化方法是不会将该字段进行编码的;同理在反序列化的时候,如果在反序列化序列中没有找到message中某一字段的值,那么protobuf会用该字段的默认值来填充该字段;
下面是各个类型对应的默认值:
类型 | 默认值 |
string | 空串 |
bytes | 空字节 |
bool | false |
数值类型(int32、int64、uint32、sint32、float、double等) | 0 |
枚举类型 | 第一个枚举常量 |
自定义类型 | 它的取值依赖于对应语言 |
对于被repeated修饰的字段 | 空列表 |
对于消息字段、oneof字段、any字段 | C++、Java中都有相应的has_xxx()方法来检测当前字段是否被设置 |
9.更新消息
9.1 更新规则
如果现有的消息类型已经不满足我们的业务需求了,我们可以对消息类型进行更改,比如:新增一个字段、修改一个字段、删除一个字段,当然,我们不能随意的更新消息类型,我们需要遵循一定的规则,否则我们的程序有可能出现数据紊乱、数据丢失的现象;
更新规则:
1.禁止修改已有字段的编号;
通过前文的学习,我们可以知道protobuf在编码字段的时候实际上是将字段值、字段类型、字段编号一起进行编码的,而在反序列化的时候就是根据反序列化中的字段编号将字段值反序列化到对应字段,如果我们更改了已存在的字段的编号,那么很有可能造成数据错位或者数据丢失;
eg:
2.如果我们只更改message中字段的类型,那么对于如下更改反序列化的字段值是兼容的:
int32、uint32、int64、bool等是完全兼容的,可以从这些类中的一个转换为另一个;
eg:
3.sin32和sin64兼容但是与其它整型不兼容、fixed32与sfixed32兼容但是与其它整型不兼容、fixed64与sfixed64兼容但是与其它整型不兼容;
4.string 和bytes在合法UTF-8字节下也是兼容的;
5.enum与int32,uint32,int64和uint64兼容(注意若值不匹配会被截断)。但要注意当反序列化消息时会根据语⾔采⽤不同的处理⽅案:例如,未识别的proto3枚举类型会被保存在消息中,但是当消息反序列化时如何表⽰是依赖于编程语⾔的。整型字段总是会保持其的值。
6.oneof:
①将一个独立的字段移动到一个新的oneof类型中去是安全的和二进制兼容的;
②若明确我们的代码中不会对多个字段同时设置值,那么可以将这多个字段移入到一个新的oneof类型中:
③如果我们的代码中已经明确了会对多个字段进行同时设置,但我们依旧将这多个字段移入进一个新的oneof类型,那么可能会导致数据丢失;
9.2 保留字段
在消息类型中如果我们要删除某一个字段的话,我们不能直接将其注释掉或者删除掉,如果我们这样做了会有数据紊乱、数据损坏的问题;
主要原因就是:
如果我们直接将某一个字段进行了删除,那么未来某一天用户在添加新字段的时候这个新字段可能会使用被删除字段的编号,这样的话就会造成原来我们的旧数据信息会跑到我们的新字段中去;这是不被允许的;
为此,为了避免不合理的情况发生,我们坚决不能使用已删除的字段的字段编号,可是时间旧了我们怎么知道那些字段编号是已被删除的字段编号呢?
为了解决这个问题Protobuf为我们提供了一个关键字:reserved,经过reserved修饰的字段编号为保留protobuf的保留编号,如果我们后续使用保留编号的话在编译的时候protobuf会发生语法错误;
为此,当我们在删除一个字段过后,一定要及时的将该字段的编号设置为保留编号,避免被后面误用;
9.3 未知字段
什么是未知字段?
本来,proto3在解析消息时总是会丢弃未知字段,但在3.5版本中重新引⼊了对未知字段的保留机
制。所以在3.5或更⾼版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果
中。
9.3.1 未知字段的获取
MessageLite类:
1.MessageLite从名字看是轻量级的message,仅仅提供序列化、反序列化功能
2.类定义在google提供的message_lite.h中.
Message类:1.我们自定义的message类都是继承于Message类;
2.Message类中最重要的两个接口GetDescriptor/GetReflection,可以获取该类型对应的Descriptor对象指针和Reflection对象指针。
3.类定义在google提供的message.h中。
Descriptor类:1.是对与我们自定义的message类的描述,包括自定义message的名字、所有字段的描述、原始的proto文件等;
2.类定义在google提供的descriptor.h中
Reflection类:1.主要提供了动态读写消息字段的接⼝,对消息对象的⾃动读写主要通过该类完成。
2.提供⽅法来动态访问/修改message中的字段,对每种类型,Reflection都提供了⼀个单独的接⼝⽤于读写字段对应的值。
3.类中还包含了访问/修改未知字段的⽅法
4.定义在google提供的message.h中
UnknownFieldSet类:1.UnknownFieldSet包含在分析消息时遇到但未由其类型定义的所有字段;
2.若要将UnknownFieldSet附加到任何消息,请调⽤Reflection::GetUnknownFields()。
3.类定义在unknown_field_set.h中;
UnknownField类:1.表⽰未知字段集中的⼀个字段;
2.类定义在unknown_field_set.h中;
9.3.2 打印未知字段
流程如下:
首先调用Message类中的GetReflection接口获取Reflection对象,然后再调用Reflection对象的GetUnknownFields接口获取未知字段集合对象(UnknownFieldset),然后遍历这个集合对象获取每一个UnknownField元素,在根据每一个UnknownField元素获取未知字段,一个未知字段对应一个UnknownField元素;
为了营造出未知字段的情况,我们按照如下方法进行:
步骤:
1.向文件中序列化一些正常数据:
2.更改.proto文件
删除.proto文件里的age字段,然后再用这个新的消息类型去进行反序列化log.bin文件中的数据,那么对于数据值:23来说它应该被放入编号为2的字段中,但是消息类型中没有编号为2的字段,因此数据值:23会被放入未知字段中,因此我们此时打印未知字段打印出来的值应该也是23:
3.编写代码:
运行程序:
结果与我们预期一致。
9.4 前后兼容性
前后兼容的作⽤:当我们维护⼀个很庞⼤的分布式系统时,由于你⽆法同时升级所有模块,为了保证在升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的“向后兼容”或“向前兼
容”。
10. option选项
proto⽂件中可以声明许多选项,使⽤ option 标注。选项能影响proto编译器的某些处理⽅式。
选项分为⽂件级、消息级、字段级等等,但并没有⼀种选项能作⽤于所有的类型.
五.总结
1.序列化能⼒对比
1.XML、JSON、ProtoBuf 都具有数据结构化和数据序列化的能力。
2. XML、JSON 更注重数据结构化,关注可读性和语义表达能力。ProtoBuf 更注重数据序列化,关注 效率、空间、速度,可读性差,语义表达能力不⾜,为保证极致的效率,会舍弃⼀部分元信息。
3. ProtoBuf 的应⽤场景更为明确,XML、JSON的应⽤场景更为丰富。
原文链接:https://blog.csdn.net/qq_62106937/article/details/134095333