目录
一.初识ProtoBuf
1.序列化和反序列化的概念
序列化: 就是将一个对象转换为字节序的过程;
反序列化 就是将一个字节序转换为一个完整对象的过程;
如何实现序列化?
主流的序列化和反序列化工具有:XML、JSON、ProtoBuf;
2.什么是ProtoBuf
Protocol Buffers是Google的⼀种语⾔⽆关、平台⽆关、可扩展的序列化结构数据的⽅法,它可⽤于(数据)通信协议、数据存储等。
Protocol Buffers类⽐于XML,是⼀种灵活,⾼效,⾃动化机制的结构数据序列化⽅法,但是⽐XML更⼩、更快、更为简单。
你可以定义数据的结构,然后使⽤特殊⽣成的源代码轻松的在各种数据流中使⽤各种语⾔进⾏编写和读取结构数据。你甚⾄可以更新数据结构,⽽不破坏由旧数据结构编译的已部署程序
3.ProtoBuf的工作特点
当我们引入ProtoBuf工具过后,我们就只需要在 .proto文件中定义我们的类即可,不需要为这个类设计任何方法,再使用ProtoBuf的编译器来编译这个.proto文件,就会得到一个.cpp和.hpp的文件,这两个文件中分别放着我们当初在.proto文件中定义的那个类的C++的实现和声明,并且ProtoBuf的编译器在编译的过程中会为我们在.proto文件中定义的那个类自动生成序列化和反序列化方法以及一些与属性字段相关的方法,我们只需要在我们的工作代码中包含以下ProtoBuf编译出来的.cpp和.hpp文件就可以了,在我们的工作代码中就能直接使用,我们定义的类的序列化和反序列化的方法了;
从而省去了我们程序员自己开发一个类的序列化和反序列化的工作;
二.学习思路
对ProtoBuf的完整学习,将使⽤项⽬推进的⽅式完成学习:即对于ProtoBuf知识内容的展开,会对⼀个项⽬进⾏⼀个版本⼀个版本的升级去讲解ProtoBuf对应的知识点.
在后续的内容中,将会实现⼀个通讯录项⽬。对通讯录⼤家应该都不陌⽣,⼀般,通讯录中包含了⼀批的联系⼈,每个联系⼈⼜会有很多的属性,例如姓名、电话等等。
随着对通讯录项⽬的升级,我们对ProtoBuf的学习与使⽤就越深⼊。
三.快速上手
我们将联系人信息类先定义在.proto
文件中,然后利用protoc编译器编译这个文件,然后我们在自己的工作代码中引用这个编译出来的类就好。联系人信息类在经过protoc编译过后会自动变为含有序列化和反序列化方法的C++类,我们只需要直接调用现成的方法即可。
步骤如下:
1. 创建一个.proto
文件:
这里我们创建一个PeopleInfo.proto文件,该文件用来定义联系人信息类;
2. 我们需要在这个PeopleInfo.proto
文件中完成一些初始化工作。比如:指定PB语法版本、为当前.proto
文件中的数据指定作用域;
① 我们可以利用syntax
关键字来指定当前.proto
文件锁采用的语法版本
② 我们也可以为当前文件中定义的类声明一个命名空间,来避免不同 .proto之间的命名冲突的问题;
这一点我们可以利用关键字package来实现:
3. 初始化工作都完成的差不多了,我们就可以开始定义类了,在.proto
文件中定义类是利用message
关键字来完成的。
4. 利用protoc编译PeopleInfo.proto文件
编译命令:
protoc [--proto_path=IMPORT_PATH] --cpp_out=DST_DIR path/to/file.proto
最后编译出如下文件:
之后我们只需要在我们的C++代码中包含PeopleInfo.pb.h这个头文件就能在我们的业务代码中使用PeopleInfo类的序列化和反序列化方法。
我们进一步阅读.h文件:
package定义的命名空间会转换为C++中的namespase命名空间
每个字段都有设置和获取的方法,getter的名称与小写字段完全相同,setter方法以set_开头。
每个字段都有⼀个clear_方法,可以将字段重新设置回empty状态
在消息类的⽗类MessageLite 中,提供了读写消息实例的⽅法,包括序列化⽅法和反序列化⽅法。
5.编写mian.cc文件代码
所有准备工作都已经准备好了,接着我们开始着手使用序列化和反序列化方法,我们的目的是将一个联系人的信息进行序列化,然后打印序列化结果,然后再反序列化,打印出反序列化的结果,为此我们的代码可以这样写:
6.编译工作
然后我们采用g++编译器进行编译,编译的时候一定不要忘记链接protobuf库,不然会报连接错误
运行结果:
四.proto3语法详解
在语法详解部分,依旧使⽤项⽬推进的⽅式完成学习。这个部分会对通讯录进⾏多次升级,使⽤2.x表⽰升级的版本,最终将会升级如下内容:
1.不再打印联系⼈的序列化结果,⽽是将通讯录序列化后并写⼊⽂件中:
2.从⽂件中将通讯录解析出来,并进⾏打印
3.新增联系⼈属性,共包括:姓名、年龄、电话信息、地址、其他联系⽅式、备注
1.字段规则
singular:消息中可以包含该字段零次或⼀次(不超过⼀次)。proto3语法中,字段默认使⽤该规则。
repeated :消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。说大白话就是利用该规则可以定义数组。
2.消息类型的定义与使用
在单个.proto文件中可以定义多个message,同时proto3语法也支持嵌套1定义message,并且不限制嵌套深度;
单个.proto文件中可以定义多个message
可嵌套定义message
可导入其他文件中的message
2.1代码示例
步骤:
1.在一个.proto文件中创建多个message。定义联系人类,通讯录类:
2.使用protoc编译当前的.proto文件使其生成对应的C++文件:
生成:
3.编写代码
为了方便观察结果,我们可以专门写两个程序,一个专门向通讯录进行写入的程序,一个专门从通讯录读取的程序:
write.cc:
read.cc:
makefile:
4.使用make编译程序
运行结果:
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类型伴生出那些方法:
步骤三:编写代码
write.cc新增:
read.cc:
步骤四:使用g++编译
运行结果:
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 代码示例
步骤一:编写.proto文件
步骤二:使用protoc编译文件
步骤三:编写代码
write.cc
read.cc
步骤四:使用g++编译读写文件
运行结果:
6.map类型
语法⽀持创建⼀个关联映射字段,也就是可以使⽤ map 类型去声明字段类型,格式为:
map<key_type, value_type> map_field = N;
6.1 代码示例
步骤一:编写.proto文件
步骤二:使用protoc编译文件
步骤三:编写代码
write.cc
read.cc
步骤四:使用g++编译文件
运行结果:
7.ProtoBuf生成的方法的规律
1. 如果是protoBuf的内置标量类型,那么生成常用方法如下:
2. 如果是message自定义类型,那么生成常用方法如下:
3. 如果是数组字段,那么会生成常用方法如下:
4. enum枚举字段,生成常用方法:
5. Any字段,生成常用方法:
6. onefo字段,常用方法:
7. map字段,常用方法:
8.常用序列化和反序列化方法:
常用序列化方法:
常用反序列化方法:
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 更新规则
如果现有的消息类型已经不满足我们的业务需求了,我们可以对消息类型进行更改,比如:新增一个字段、修改一个字段、删除一个字段,当然,我们不能随意的更新消息类型,我们需要遵循一定的规则,否则我们的程序有可能出现数据紊乱、数据丢失的现象;
更新规则:
9.2 保留字段
在消息类型中如果我们要删除某一个字段的话,我们不能直接将其注释掉或者删除掉,如果我们这样做了会有数据紊乱、数据损坏的问题;
主要原因就是:
如果我们直接将某一个字段进行了删除,那么未来某一天用户在添加新字段的时候这个新字段可能会使用被删除字段的编号,这样的话就会造成原来我们的旧数据信息会跑到我们的新字段中去;这是不被允许的;
为此,为了避免不合理的情况发生,我们坚决不能使用已删除的字段的字段编号,可是时间旧了我们怎么知道那些字段编号是已被删除的字段编号呢?
为了解决这个问题Protobuf为我们提供了一个关键字:reserved,经过reserved修饰的字段编号为保留protobuf的保留编号,如果我们后续使用保留编号的话在编译的时候protobuf会发生语法错误;
为此,当我们在删除一个字段过后,一定要及时的将该字段的编号设置为保留编号,避免被后面误用;
9.3 未知字段
什么是未知字段?
本来,proto3在解析消息时总是会丢弃未知字段,但在3.5版本中重新引⼊了对未知字段的保留机
制。所以在3.5或更⾼版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果
中。
9.3.1 未知字段的获取
9.3.2 打印未知字段
步骤:
1.向文件中序列化一些正常数据:
2.更改.proto文件
删除.proto文件里的age字段,然后再用这个新的消息类型去进行反序列化log.bin文件中的数据,那么对于数据值:23来说它应该被放入编号为2的字段中,但是消息类型中没有编号为2的字段,因此数据值:23会被放入未知字段中,因此我们此时打印未知字段打印出来的值应该也是23:
3.编写代码:
运行程序:
结果与我们预期一致。
9.4 前后兼容性
前后兼容的作⽤:当我们维护⼀个很庞⼤的分布式系统时,由于你⽆法同时升级所有模块,为了保证在升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的“向后兼容”或“向前兼
容”。
10. option选项
proto⽂件中可以声明许多选项,使⽤ option 标注。选项能影响proto编译器的某些处理⽅式。
选项分为⽂件级、消息级、字段级等等,但并没有⼀种选项能作⽤于所有的类型.
五.总结
1.序列化能⼒对比
原文链接:https://blog.csdn.net/qq_62106937/article/details/134095333
发表评论