ProtoBuf的学习和使用---C++
- ⼀、初识ProtoBuf
- 序列化和反序列化的概念
- ProtoBuf是什么?
- ProtoBuf工作特点
- 二、主要学习思路
- 三、快速上手
- 四、proto3语法详解
- 1.字段规则
- 2.消息类型的定义与使⽤
- 实际操练
- 3.enum枚举类型
- enum注意事项
- enum实操
- 4.Any类型
- Any类型实操
- 5.oneof类型
- oneof类型实操
- 6.map类型
- map实操
- 7.ProtoBuf生成的方法的规则总结
- 8.默认值
- 9.更新消息
- 更新规则
- 保留字段
- 未知字段
- 未知字段的获取
- 打印未知字段
- 前后兼容性
- 10.选项option
- 常用选项举例
⼀、初识ProtoBuf
序列化和反序列化的概念
序列化: 就是将一个对象转换为字节序的过程;
反序列化 就是将一个字节序转换为一个完整对象的过程;
什么时候需要用到序列化和反序列化?
- 网络传输:在网络中传输数据时,我们是不会将我们的对象这个整体数据放在网络中进行传输,这样做传输效率低不说,两个主机之间也可能存在兼容性问题造成对同一个对象解释出不同的结果,因此在真正在网络中传输之前,都是需要将我们的对象转换为字节流在进行传输的,也就是序列化,对端主机在接收到我们发送过去的字节流数据过后,会对其进行反序列化,从而恢复出原有的对象,然后再进行上层业务逻辑的处理;
- 存储数据: 当我们想把内存中某一个对象的数据存储在磁盘中时,OS实际存的并不是对象本身而是将对象序列化过后的结果来进行存储,当用户读取的时候,OS在将磁盘中的数据经过反序列化,然后交付给上层;
如何实现序列化?
主流的序列化和反序列化工具有:XML、JSON、ProtoBuf;
ProtoBuf是什么?
Protocol Buffers是Google的⼀种语⾔⽆关、平台⽆关、可扩展的序列化结构数据的⽅法,它可⽤于(数据)通信协议、数据存储等。
Protocol Buffers类⽐于XML,是⼀种灵活,⾼效,⾃动化机制的结构数据序列化⽅法,但是⽐XML更⼩、更快、更为简单。
你可以定义数据的结构,然后使⽤特殊⽣成的源代码轻松的在各种数据流中使⽤各种语⾔进⾏编写和读取结构数据。你甚⾄可以更新数据结构,⽽不破坏由旧数据结构编译的已部署程序
总结:
- 语言无关、平台无关: 即ProtoBuf支持Java、C++、Python等多种主流语言,支持Window、Linux等多个平台
- 高效: 即比XML更小、更快、更为简单。
- 扩展性好、兼容性好: 你可以更新数据结构,而不影响和破环原有的旧程序;
ProtoBuf工作特点
在正式使用ProtoBuf工具之前,我们先来了解一下ProtoBuf是如何支持序列化和反序列化的工作的(以C++为例)
假设在没有ProtoBuf序列化和反序列化工具之前,我们定义了一个类,然后想要对这个类进行序列化和反序列化工作,那么就需要我们自己在该类中设计和构造一个序列化和反序列化的方法,这样的话对于我们的开发效率会有点影响,要是这个类天生就自带的有序列化和反序列化的方法该多好!
当我们引入ProtoBuf工具过后,我们就只需要在
.proto
文件中定义我们的类即可,不需要为这个类设计任何方法,然后的话我们再使用ProtoBuf的编译器来编译这个.proto
文件,就会得到一个.cpp
和.hpp
的文件,这两个文件中分别放着我们当初在.proto
文件中定义的那个类的C++的实现和声明,并且ProtoBuf的编译器在编译的过程中会为我们在.proto
文件中定义的那个类自动生成序列化和反序列化方法以及一些与属性字段相关的方法,我们只需要在我们的工作代码中包含以下ProtoBuf编译出来的.cpp
和.hpp
文件就可以了,在我们的工作代码中就能直接使用,我们定义的类的序列化和反序列化的方法了;
从而省去了我们程序员自己开发一个类的序列化和反序列化的工作;
二、主要学习思路
对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
关键字来完成的,eg:
在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 注意:
- 上表中左边对应的是PB提供的类型,右边表示的是该PB类型在C++语法中对应的C++类型,我们在PB文件中定义类时需要使用PB语法提供的类型来定义类,而不是C++语法提供的类型;
- 上面所说的变长编码就是说,在经过protoc编译过后,原本4字节或8字节的数据可能会被以其更少的字节或更多的字节来进行编码(或序列化);举个例子:就比如我们上述在PB文件中定义的PeopleInfo类中的age字段:
在PB文件中是int32类型,但是在经过proto编译过后在C++中就对应int32类型,也就是4字节的数据,当我们在C++中调用序列化方法来编码该字段时,该序列化方法可能会用低于4字节或高于4字节的数据来进行编码,也就是说序列化方法在序列化字段的时候会判断一下当前字段在PB文件中对应的是什么类型,然后再决定采用变长编码还是定长编码的方式来进行编码;假设现在我们将age在PB文件中的类型换为sfixed32类型,那么在使用protoc编译过后在C++中对应的就是int32类型,那么此时在调用序列化编码该字段的时候就会以4字节的方式来序列化该字段,也就是定长编码的方式来序列化!
对于反序列化也是同理,反序列化方法会自动识别字段类型和编码方式,从而准确的完成反序列化操作,从而将我们的数据正确解码为C++中正确的数据类型!- 序列化方法在编码的时候除了会将当前字段的数值进行编码外还会对当前字段的PB类型、PB编号进行编码,这么做到目的就是为了方便反序列化的时候能够正确的解码出数据!
1 ~ 15
范围内的编号需要一个字节进行编码,16 ~ 2047
内的数据需要两个字节进行编码。所以1 ~ 15
要用来标记出现非常频繁的字段,要为将来有可能添加的、频繁出现的字段预留⼀些出来。4. 利用protoc编译PeopleInfo.proto文件形成对应的C++文件;
编译格式:protoc [-I] --xxx_out=OUT_DIR file.proto
-I :表示指定被编译的.proto文件所在目录,可多次指定,用空格隔开;若不指定则默认在当前路径下进行搜索;
–xxx_out: 将.proto文件编译成那种编程语言的文件;eg:--cpp_out将.proto文件编译成C++文件
、--java_out=将.proto文件编译成java文件
、--csharp_out=将.proto文件编译成C#文件
等
OUT_DIR: 编译完成过后的文件所存放的路径;
file.proto: 被编译的.proto文件eg1:
proto --cpp_out=. PeopleInfo.proto
//在当前路径下检索PeopleInfo.proto文件,然后将其编译成C++文件,将该C++文件放在挡墙路径下;(不指定检索目录的情况)eg2:
protoc -I test1/ --cpp_out=./test1/ PeopleInfo.proto
//在test1目录下检索PeopleInfo.proto文件,然后将其编译成C++文件,将该C++文件放在test1/目录下;(指定检索目录的情况)最后编译出来的文件如下:
之后我们只需要在我们的C++代码中包含PeopleInfo.pb.h这个头文件就能在我们的业务代码中使用PeopleInfo类的序列化和反序列化信息了,为了更好的使用使用PB编译出来的类,我们可以先了解一下PB编译出来的文件中都含有那些方法:
package定义的命名空间会转换为C++中的namespase命名空间,message定义的类会转换为C++中国class定义的类,同时还会增加一系列与该属性字段相关的方法:
eg:
clear_xxx(): 清除改字段的值;
xxx(): 获取当前字段的值;
set_xxx(): 为当前字段设置值;
mutable_xxx() :获取当前字段的地址,对于自定义类型来说protoc不会为其生成set_xxx()的方法来设置值,因为它没办法预测用户到底想如何给自定义类型设置值,但是它会生成对应的mutable_xxx()函数,利用mutable_xxx()函数我们可以获取到当前字段的地址,然后再利用字段的内置set_xxx()方法进行对自定义类型中的各个字段进行初始化,对于protobuf内置的类型protoc不会为其生成mutable_xxx()方法,当然string等除外,这些基本类型利用set_xxx()方法就能够设置值了。注:xxx为字段名
介绍了这么半天,我们的主角还没登场呢?
序列化和反序列化方法在哪里?
在消息类的⽗类MessageLite 中,提供了读写消息实例的⽅法,包括序列化⽅法和反序列化⽅法。
- ProtoBuf序列化的结果是二进制序列,而非文本格式,正是这一点使得ProtoBuf序列化后的结果的破译成本相比于JSON序列化后的结果的破译成本更高;
5. 所有准备工作都已经准备好了,接着我们开始着手使用序列化的反序列化方法,我们的目的是将一个联系人的信息进行序列化,然后打印序列化结果,然后再反序列化,打印出反序列化的结果,为此我们的代码可以这样写:
然后我们采用g++编译器进行编译,编译的时候一定不要忘记链接protobuf库,不然会报连接错误;
我们可以看到,我们在打印序列化数据的时候,并没有将联系人的信息完全打印出来,甚至有一些n信息被解释成了’\n’;
小结ProtoBuf使⽤流程:
- 编写.proto文件,目的是定义结构对象(message)及属性内容;
- 使用protoc编译器编译.proto文件,将我们在.proto文件中定义的类转换成对应编程语言中的类并且添加各种操作属性字段的方法和序列化、反序列化方法;
- 依赖生成的接口,将编译生成的代码引入我们的工作代码中,实现对于message中定义的字段的获取和设置,同时完成对于message对象的序列化和反序列化;
总的来说:ProtoBuf是需要依赖通过编译⽣成的头⽂件和源⽂件来使⽤的。有了这种代码⽣成机制,开发⼈员再也不⽤吭哧吭哧地编写那些协议解析的代码了(⼲这种活是典型的吃⼒不讨好)。
四、proto3语法详解
在语法详解部分,依旧使⽤项⽬推进的⽅式完成学习。这个部分会对通讯录进⾏多次升级,使⽤2.x表⽰升级的版本,最终将会升级如下内容:
- 不再打印联系⼈的序列化结果,⽽是将通讯录序列化后并写⼊⽂件中:
- 从⽂件中将通讯录解析出来,并进⾏打印
- 新增联系⼈属性,共包括:姓名、年龄、电话信息、地址、其他联系⽅式、备注
1.字段规则
- singular:消息中可以包含该字段零次或⼀次(不超过⼀次)。proto3语法中,字段默认使⽤该规则。(实测在proto3语法中不可显示加上该规则):
然后的话,我再用大白话解释一下singular规则,简单来说就是一个字段在如果被singular修饰,那么该字段在序列化的时候,如果从来没有被设置过值的话,那么在序列化的时候就不会将该字段序编码进序列化序列,同理当反序列化的时候,我们在反序列化序列中没有找到被singular修饰的字段对应的编码值,那么protobuf会使用该字段的默认值来填充该字段;- repeated :消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。说大白话就是利用该规则可以定义数组;
eg:
现在我们的PeopleInfo中含有name、age字段,现在我想向其中添加电话号码字段,我们可以在message中定义一个string phone=3;但是一个用户可能不止一个手机号啊,因此我们可以使用一个数组来存储用户的电话号码,因此我们可以将我们的phone改为一个数组类型:repedted string Phones=3;
接着我们可以来看一看数组类型在在经过protoc编译过后会拥有那些方法:
主要方法如下:
clear_xxx();清理数组;
xxx(index);返回下标为index的值,类似于xxx[index];
mutable_xxx(index);但会下标index处的地址;
xxx_size();返回数组的元素个数
add_xxx();返回最后一个待插入位置的地址;
set系列的方法,只有当数组元素类型为protobuf内置类型时才有;
注:xxx为字段名
2.消息类型的定义与使⽤
在单个.proto文件中可以定义多个message,同时proto3语法也支持嵌套1定义message,并且不限制嵌套深度;
eg:
- 单个.proto文件中可以定义多个message
- 支持嵌套定义message;
只要保证同级字段之间不同名不同编号即可;- 支持导入其它.proto文件中的message来进行定义:
导入其它.proto文件,语法:import path/xxx.proto
引入的文件声明了package,使用其类型时需要用[命名空间 . 消息类型]的格式;
实际操练
接着我们创建一个通讯录2.0版本对于上面两个语法进行操练;
通讯录2.0版本要求:
- 将联系人信息放入通讯录,然后将通讯录进行序列化,将序列化结果写入文件中;
- 在写一个读程序将序列化结果从通讯录中读取出来,并打印;
- 联系人信息包括,姓名、年龄、电话等字段;
为此,我们可以在两个.proto文件中进行定义不同的message,在PeopleInfo.proto文件中定义联系人类,然后在Contacts.proto文件中定义通讯录类:
实际上的话,针对于PeopleInfo我们可以进行适当的优化,就是一个人可能不止有一个电话,可能有多个,因此,对于phone这一字段,我们建议使用数组来保存,也就是使用repeated字段来修饰:
可是我们觉得这还不够好,因为我们想要详细记录某一个人的电话信息,比如一个电话号码有它所归属的运营商,因此在存储电话信息的时候,除了存储电话号码之外,我们还建议间电话号码的运营商信息也存储起来,为此我们可以专门定义一个电话类来专门存储电话信息;
到这,我们的在protoBuf中的工作就已经完成了,接着,我们使用protoc编译当前的.proto文件使其生成对应的C++文件:
紧接着我们只需要在我们的代码中进行包含对应的头文件就可以使用我们的PeopleInfo和Contacts对象了;
为了方便观察结果,我们可以专门写两个程序,一个专门向通讯录进行写入的程序,一个专门从通讯录读取的程序:
eg:
//写程序:
//读程序:
//makefile
我们可以去直接查看contacts.bin文件:
发现里面存的全是二进制,查看二进制文件,我们可以利用命令:
hexdump -C xxx.bin
文件来查看二进制文件;
hexdump是Linux下的一个二进制文件查看工具,它可以将二进制文件转换为ASCII、八进制、十进制、十六进制格式进行查看。
-C:将每字节数据以16进制和ASCII码形式展示出来:
当然,对于序列化进文件的数据,如果我们每次都写一个read工具来查看的话,比较耗费时间,有没有现成的工具来给我是查看序列化的数据呢?答案是有的,protoc 给我们提供了–decode选项,该选项的大概功能就是:允许我们从标准输入读取给定类型的二进制消息,然后它会将读取到的二进制消息进行解码,然后将解码结果输出到标准输出;(其中给定类型必须是在.proto文件中出现的类型)
具体用法如下:
protoc --decode=MESSAGE_TYPE MESSAGE_TYPE所在文件
MESAGE_TYPE:表示给定的消息类型,这里我们希望他从contacts.bin中读取二进制数据,因为我们需要将protoc的标准输入重定向到contacts.bin中去,具体命令如下:
protoc --decode=MySpace.Contacts Contacts.proto < contacts.bin
:
3.enum枚举类型
proto3支持我们定义枚举类型并使用,在.proto文件中的写法就是:
enum xxx{
YYY=0;
ZZZ=1;
//...
}
- 与C++中定义的枚举类型差不多,但是这里要注意,在proto文件中定义的枚举类型的第一个常量值必需是0,同时proto也会把enum类型的第一个枚举常量作为enum字段的默认值!
- 枚举类型可以在message外面定义,也可以在message 内部嵌套定义;
- 枚举类型的常量值在32位整数之间,但是负数无效因而不建议使用,这与ProtoBuf的编码规则有关;
- proto文件中的枚举类型不会自增,因此每个枚举类型都需要我们亲自赋值,同时需要以分号(;)结尾;
enum注意事项
- 同级的枚举类型不能还有相同名称的枚举常量:
- 在同一个.proto文件中,最外成枚举类型和嵌套定义的枚举类型不算同级,即使处于同级但是处于不同作用域下的枚举常量也不算重复定义:
3. 多个.proto文件下,若一个文件引入了其它文件,且最外层的枚举类型含有相同的枚举常量,算同级:
4. 解决第3条的办法就是让具有相同的枚举常量的枚举类型处于不同作用域下或者处于同一作用域下的不同级别下即可:
这里我们选择让其处于不同作用域下,eg:我们可以修改PeopleInfo.proto文件中的package:
这样我们的编译就可以完美通过了,当然在编译的时候可能会出现:
这是由于我们在Contacts.proto中导入了enum A但是却为使用它而报出的警告,我们忽略即可;
enum实操
在上一版的通讯录中,我们的联系人信息包括了:姓名、年龄、电话信息;
其中电话信息中包括,电话号码和电话号码类型,对于电话号码类型我们是利用string的方式来进行存储的,现在我们可以考虑使用enum来进行替换:
然后我们编译这个proto文件,再来看看enum类型又会伴生出那些方法:
首先,对于枚举类型,protoc为枚举类西编译出了一些全局函数,其中比较常用的就是:
枚举类型名_IsValid(value)
//判断values值是不是枚举常量
枚举类型名_Name(enum_value)
//将枚举常量转换为字符串;
其次就是在Phone类里面,也会生成:
clear_xxx();
xxx();
set_xxx();
等操作枚举字段的方法;
接下来,就是我们正式应用的时刻了;
//write程序:
//read程序:
程序运行结果:
实际上这一点,可以用之前的修饰规则来解释,在proto3语法中每个字段默认都是有singular规则修饰的,因此被改规则修饰的字段在序列化的时候如果没有设置值,那么该字段不会被序列化,同理,如果在该字段的反序列化序列中找不到对应字段的编码值,那么该字段会被使用当前类型的默认值来进行填充,而我们的张三的反序列化数据中是没有枚举数据类型的,因此在反序列化的时候,对于张三的枚举字段则是用枚举类型的默认值来填充的,而当前枚举类型的默认值就是ZG_YD,因此我们才看到张三数据在枚举字段显示的是ZG_YD;
4.Any类型
Any类型可以理解为一个泛类型,利用Any类型定义的字段可以接收任意类型的值;
Any类型本质上就是ProtoBuf中的一个自定义message,由potobuf官方为我们定好的,因此当我们想要使用该类型的时候,我们需要导入Any.proto:
import "google/protobuf/any.proto";
Any类型实操
现在我们又要升级我们的通讯录版本了,在上一版本的通讯录中,联系人信息包含:姓名、年龄、电话信息,这次我们需要向联系人信息中添加地址信息,然后的话,地址信息中又包含:家庭地址、工作地址;
而在联系人信息中,我们准备使用Any类型来接收地址信息,因此我们的PeopleInfo.proto文件可以按照如下这样写:
然后我们来编译这个.proto文件,看一看Any类型字段会伴生出那些方法:
常规情况:
clear_xxx();//清除Any字段的值
xxx();//获取Any字段的值;
mutable_xxx();//获取Any字段的地址
has_xxx();//当前Any字段是否被设置?
接着我们来看一看Any类内部又有那些方法:
这里我们主要关注:
PackFrom()、UnpackTo()、Is这三个函数即可;
PackFrom();//将其它message类型赋值给Any字段;
UnpackTo();//将Any字段的值赋值给其它message类型;
template< class T>
Is();//判断Any类型中存储的是不是T类型的数据;
介绍完这些,我们就该来正式使用以下这些接口了:
//write程序:
//read程序
最后运行结果:
5.oneof类型
如果消息中有很多可选字段,但是将来只会有一个字段被设置,那么就可以利用oneof类型加强这一行为,也能有节约内存的效果;
oneof类型实操
在上一版本中的通讯录中,联系人对象具有:姓名、年龄、电话、地址等信息,现在我们有个新需求,我们需要向联系人中添加一个新字段(其它联系方式字段),该字段可以存储除了电话号码之外其它的联系方式,比如QQ、微信等,但是这个其它联系方式字段只能记录多种社交联系方式中的一种,也就是说,该字段要么保留QQ号要么保留微信号,不能两个一起保存,如果你对其先进行设置了QQ号然后又对其进行设置了微信号,那么该字段会以最后一次设置的值进行保留,反之亦是如此;
为此,我们的PeopleInfo.proto文件可以可以按照如下方式写:
注意我们在定义oneof字段的格式:
这个与其他类型的定义格式不同;
格式:
oneof 字段名{
字段1;
字段2;
...
}
注意:
- 其中oneof字段中的字段与外层的其它字段同级,因此在给oneof字段内的字段设置编号的时候注意不要重复编号;
- oneof字段中的值不能使用repeated规则修饰;
接着,我们来编译我们的PeopleInfo和Contacts文件,看一看oneof字段会伴随那些方法:
首先针对oneof字段中的内部字段来说,具有常规的:
clear_xxx();//清理方法
set_xxx();//设置方法;
xxx();//读取方法;
但是这里我们要注意两个比较特俗的函数:
clear_XXX();//清理oneof字段中的值;
XXX_case();//获取oneof字段最后设置了哪一个字段,返回值是个枚举类型,protoc在编译oneof字段的时候会为oneof字段中的内部字段单独编译出来一个枚举类型:
注意上面的clear_xxx()、set_xxx()、xxx()等方法都是针对oneof字段内部字段的方法;
clear_XXX()、XXX_case();//等方法是针对oneof整个字段的方法;
尽管如此,但是这两种方法是同级的;
//write程序:
//read程序:
程序运行结果:
6.map类型
语法⽀持创建⼀个关联映射字段,也就是可以使⽤?map?类型去声明字段类型,格式为:
map<key_type, value_type> map_field = N;
其中:
- key_type:必须是除了float、bytes外的标量类型;
value_type: 无类型限制- map字段不能使用repeated字段进行修饰
- map存入的元素是无序的;
map实操
根据上一版本的通讯录中,联系人信息包括:姓名、年龄、电话信息、地址信息、其它联系方式字段,现在有一个新需求,我们期望给联系人信息中新增备注字段,这一点的话我们可以利用map类型来实现,其中以标题作为map的K值,内容作为map的value值;
因此,我们的PeopleInfo.proto文件可以按照如下方式写:
我们接着来编译PeopleInfo.proto文件,看一看map类型会伴随着那些方法:
从中可以看到,主要包含:
clear_xxx();//清理方阿飞;
xxx_size();//个数方法;
xxx();//获取map对象方法(注意获取出来的是一个const map);
mutable_xxx();//获取map对象的地址(非const指针)
//write程序:
//read程序:
程序运行结果:
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.更新消息
更新规则
如果现有的消息类型已经不满足我们的业务需求了,我们可以对消息类型进行更改,比如:新增一个字段、修改一个字段、删除一个字段,当然,我们不能随意的更新消息类型,我们需要遵循一定的规则,否则我们的程序有可能出现数据紊乱、数据丢失的现象;
更新规则:
- 禁止修改已有字段的编号;
通过前文的学习,我们可以知道protobuf在编码字段的时候实际上是将字段值、字段类型、字段编号一起进行编码的,而在反序列化的时候就是根据反序列化中的字段编号将字段值反序列化到对应字段,如果我们更改了已存在的字段的编号,那么很有可能造成数据错位或者数据丢失;
eg:
- 如果我们只更改message中字段的类型,那么对于如下更改反序列化的字段值是兼容的:
int32、uint32、int64、bool等是完全兼容的,可以从这些类中的一个转换为另一个;eg:
- sin32和sin64兼容但是与其它整型不兼容、fixed32与sfixed32兼容但是与其它整型不兼容、fixed64与sfixed64兼容但是与其它整型不兼容;
- string 和bytes在合法UTF-8字节下也是兼容的;
- enum与int32,uint32,int64和uint64兼容(注意若值不匹配会被截断)。但要注意当反序列化消息时会根据语⾔采⽤不同的处理⽅案:例如,未识别的proto3枚举类型会被保存在消息中,但是当消息反序列化时如何表⽰是依赖于编程语⾔的。整型字段总是会保持其的值。
- oneof:
①将一个独立的字段移动到一个新的oneof类型中去是安全的和二进制兼容的;
②若明确我们的代码中不会对多个字段同时设置值,那么可以将这多个字段移入到一个新的oneof类型中:
③如果我们的代码中已经明确了会对多个字段进行同时设置,但我们依旧将这多个字段移入进一个新的oneof类型,那么可能会导致数据丢失;
保留字段
在消息类型中如果我们要删除某一个字段的话,我们不能直接将其注释掉或者删除掉,如果我们这样做了会有数据紊乱、数据损坏的问题;
主要原因就是:
如果我们直接将某一个字段进行了删除,那么未来某一天用户在添加新字段的时候这个新字段可能会使用被删除字段的编号,这样的话就会造成原来我们的旧数据信息会跑到我们的新字段中去;这是不被允许的;
eg:
为此,为了避免不合理的情况发生,我们坚决不能使用已删除的字段的字段编号,可是时间旧了我们怎么知道那些字段编号是已被删除的字段编号呢?
为了解决这个问题Protobuf为我们提供了一个关键字:reserved,经过reserved修饰的字段编号为保留protobuf的保留编号,如果我们后续使用保留编号的话在编译的时候protobuf会发生语法错误;
为此,当我们在删除一个字段过后,一定要及时的将该字段的编号设置为保留编号,避免被后面误用;
eg:
当然除了上诉设置保留编号的方式,我们还可以按照如下方法进行设置:
未知字段
什么是未知字段?
举个例子:
本来,proto3在解析消息时总是会丢弃未知字段,但在3.5版本中重新引⼊了对未知字段的保留机
制。所以在3.5或更⾼版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果
中。
未知字段的获取
MessageLite类:
- MessageLite从名字看是轻量级的message,仅仅提供序列化、反序列化功能
- 类定义在google提供的message_lite.h中.
Message类:
- 我们自定义的message类都是继承于Message类;
- Message类中最重要的两个接口GetDescriptor/GetReflection,可以获取该类型对应的Descriptor对象指针和Reflection对象指针。
- 类定义在google提供的message.h中。
Descriptor类:
- 是对与我们自定义的message类的描述,包括自定义message的名字、所有字段的描述、原始的proto文件等;
- 类定义在google提供的descriptor.h中
Reflection类:
- 主要提供了动态读写消息字段的接⼝,对消息对象的⾃动读写主要通过该类完成。
- 提供⽅法来动态访问/修改message中的字段,对每种类型,Reflection都提供了⼀个单独的接⼝⽤于读写字段对应的值。
针对所有不同的field类型 FieldDescriptor::TYPE_* ,需要使⽤不同的 Get*()/Set*()/Add*() 接⼝;
repeated类型需要使⽤ GetRepeated*()/SetRepeated*() 接⼝,不可以和⾮repeated类型接⼝混⽤;
message对象只可以被由它⾃⾝的 reflection(message.GetReflection()) 来操作;
- 类中还包含了访问/修改未知字段的⽅法
- 定义在google提供的message.h中
UnknownFieldSet类:
- UnknownFieldSet包含在分析消息时遇到但未由其类型定义的所有字段;
- 若要将UnknownFieldSet附加到任何消息,请调⽤Reflection::GetUnknownFields()。
- 类定义在unknown_field_set.h中;
UnknownField类:
- 表⽰未知字段集中的⼀个字段;
- 类定义在unknown_field_set.h中;
打印未知字段
流程如下:
首相调用Message类中的GetReflection接口获取Reflection对象,然后再调用Reflection对象的GetUnknownFields接口获取未知字段集合对象(UnknownFieldset),然后遍历这个集合对象获取每一个UnknownField元素,在根据每一个UnknownField元素获取未知字段,一个未知字段对应一个UnknownField元素;
为了营造出未知字段的情况,我们按照如下方法进行:
为此我们向向文件里面序列化一些正常数据:
紧接着,我删除age字段,然后再用这个新的消息类型去进行反序列化log.bin文件中的数据,那么对于数据值:23来说它应该被放入编号为2的字段中,但是消息类型中没有编号为2的字段,因此数据值:23会被放入未知字段中,因此我们此时打印未知字段打印出来的值应该也是23:
修改过后的proto文件:
打印未知字段程序如下:
程序运行结果:
很显然,结果是我们预期的!
前后兼容性
前后兼容的作⽤:当我们维护⼀个很庞⼤的分布式系统时,由于你⽆法同时升级所有模块,为了保证在升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的“向后兼容”或“向前兼
容”。
10.选项option
proto⽂件中可以声明许多选项,使⽤ option 标注。选项能影响proto编译器的某些处理⽅式。
选项分为⽂件级、消息级、字段级等等,但并没有⼀种选项能作⽤于所有的类型.
常用选项举例
optimize_for:该选项为⽂件选项,可以设置protoc编译器的优化级别,分别为SPEED 、
CODE_SIZE 、 LITE_RUNTIME 。受该选项影响,设置不同的优化级别,编译.proto⽂件后⽣
成的代码内容不同。SPEED: protoc编译器将⽣成的代码是⾼度优化的,代码运⾏效率⾼,但是由此⽣成的代码
编译后会占⽤更多的空间。 SPEED是默认选项。
CODE_SIZE :proto编译器将⽣成最少的类,会占⽤更少的空间,是依赖基于反射的代码来
实现序列化、反序列化和各种其他操作。但和 SPEED 恰恰相反,它的代码运⾏效率较低。这
种⽅式适合⽤在包含⼤量的.proto⽂件,但并不盲⽬追求速度的应⽤中。
LITE_RUNTIME :⽣成的代码执⾏效率⾼,同时⽣成代码编译后的所占⽤的空间也是⾮常
少。这是以牺牲ProtocolBuffer提供的反射功能为代价的,仅仅提供encoding+序列化功能,
所以我们在链接BP库时仅需链接libprotobuf-lite,⽽⾮libprotobuf。这种模式通常⽤于资源
有限的平台,例如移动⼿机平台中。语法: option optimize_for=LITE_RUNTIME;
allow_alias:允许将相同的常量值分配给不同的枚举常量,⽤来定义别名。该选项为枚举选项。
举个例⼦: