文章目录
- 1 :peach:初识 ProtoBuf:peach:
- 1.1 :apple:序列化概念:apple:
- 1.2 :apple:ProtoBuf 是什么:apple:
- 1.3 :apple:ProtoBuf 的使用特点:apple:
- 2 :peach:创建 .proto ⽂件:peach:
- 3 :peach:编译 .proto 文件:peach:
- 3 :peach:序列化与反序列化的使用:peach:
1 🍑初识 ProtoBuf🍑
1.1 🍎序列化概念🍎
序列化和反序列化
- 序列化:把对象转换为字节序列的过程称为对象的序列化。
- 反序列化:把字节序列恢复为对象的过程称为对象的反序列化。
什么情况下需要序列化
- 存储数据:当你想把的内存中的对象状态保存到⼀个⽂件中或者存到数据库中进行持久化时。
- ⽹络传输:⽹络直接传输数据,但是⽆法直接传输对象,所以要在传输前序列化,传输完成后反序列化成对象。例如我们之前学习过 socket 编程中发送与接收数据。
如何实现序列化
常见的有xml
、yml
、json
、 protobuf
1.2 🍎ProtoBuf 是什么🍎
我们先来看看官⽅给出的答案是什么?
- Protocol Buffers 是 Google 的⼀种语⾔⽆关、平台⽆关、可扩展的序列化结构数据的⽅法,它可⽤于(数据)通信协议、数据存储等。
- Protocol Buffers 类⽐于 XML,是⼀种灵活,⾼效,⾃动化机制的结构数据序列化⽅法,但是⽐XML 更⼩、更快、更为简单。
- 你可以定义数据的结构,然后使⽤特殊⽣成的源代码轻松的在各种数据流中使⽤各种语⾔进⾏编写和读取结构数据。你甚⾄可以更新数据结构,⽽不破坏由旧数据结构编译的已部署程序。
简单来讲, ProtoBuf(全称为 Protocol Buffer)是让结构数据序列化的⽅法,其具有以下特点:
- 语⾔⽆关、平台⽆关:即 ProtoBuf ⽀持 Java、C++、Python 等多种语⾔,⽀持多个平台。
- ⾼效:即⽐ XML 更⼩、更快、更为简单。
- 扩展性、兼容性好:你可以更新数据结构,⽽不影响和破坏原有的旧程序。
1.3 🍎ProtoBuf 的使用特点🍎
- 编写
.proto
⽂件,⽬的是为了定义结构对象(message
)及属性内容。 - 使⽤ protoc 编译器编译
.proto
⽂件,⽣成⼀系列接⼝代码,存放在新⽣成头⽂件和源⽂件中。 - 依赖⽣成的接⼝,将编译⽣成的头⽂件包含进我们的代码中,实现对
.proto
⽂件中定义的字段进⾏设置和获取,和对message
对象进⾏序列化和反序列化。
总的来说:ProtoBuf 是需要依赖通过编译⽣成的头⽂件和源⽂件来使⽤的。
2 🍑创建 .proto ⽂件🍑
⽂件规范
- 创建
.proto
⽂件时,⽂件命名应该使⽤全⼩写字⺟命名,多个字⺟之间⽤ _ 连接。比如:lower_snake_case.proto
- 书写
.proto
⽂件代码时,应使⽤ 2 个空格的缩进。
指定 proto3 语法
Protocol Buffers 语⾔版本3,简称 proto3,是 .proto
⽂件最新的语法版本。proto3 简化了 ProtocolBuffers 语⾔,既易于使⽤,⼜可以在更⼴泛的编程语⾔中使⽤。它允许你使⽤ Java,C++,Python等多种语⾔⽣成 protocol buffer 代码。
在 .proto
⽂件中,要使⽤ syntax = "proto3"
; 来指定⽂件语法为 proto3,并且必须写在除去注释内容的第⼀⾏。 如果没有指定,编译器会使⽤proto2语法。
package 声明符
package
是⼀个可选的声明符,能表⽰ .proto ⽂件的命名空间,在项⽬中要有唯⼀性。它的作⽤是为了避免我们定义的消息出现冲突。(类似于C++中的namespace)
比如我们就可以像这么写:
syntax = "proto3";
package contacts;
定义消息(message)
消息(message): 要定义的结构化对象,我们可以给这个结构化对象中定义其对应的属性内容。
这⾥再提⼀下为什么要定义消息?在⽹络传输中,我们需要为传输双⽅定制协议。定制协议说⽩了就是定义结构体或者结构化数据,⽐如,tcp,udp 报⽂就是结构化的。再⽐如将数据持久化存储到数据库时,会将⼀系列元数据统⼀⽤对象组织起来,再进⾏存储。
消息类型命名规范:使⽤驼峰命名法,⾸字⺟⼤写。
定义消息字段
在 message 中我们可以定义其属性字段,字段定义格式为:字段类型 字段名 = 字段唯⼀编号
;
- 字段名称命名规范:全小写字⺟,多个字⺟之间⽤
_
连接。 - 字段类型分为:标量数据类型 和 特殊类型(包括枚举、其他消息类型等)。
- 字段唯⼀编号:⽤来标识字段,⼀旦开始使⽤就不能够再改变。
该表格展⽰了定义于消息体中的标量数据类型,以及编译 .proto
⽂件之后⾃动⽣成的类中与之对应的字段类型。在这⾥展⽰了与 C++ 语⾔对应的类型:
.proto Type | Notes | C++ Type |
---|---|---|
double | double | |
float | float | |
int32 | 使⽤变⻓编码[1]。负数的编码效率较低⸺若字段可能为负值,应使⽤ sint32 代替 | int32 |
int64 | 使⽤变⻓编码[1]。负数的编码效率较低⸺若字段可能为负值,应使⽤ sint34 代替 | int64 |
uint32 | 使⽤变⻓编码[1]。 | uint32 |
uint64 | 使⽤变⻓编码[1]。 | uint64 |
sint32 | 使⽤变⻓编码[1]。符号整型。负值的编码效率⾼于常规的 int32 类型 | int32 |
sin64 | 使⽤变⻓编码[1]。符号整型。负值的编码效率⾼于常规的 int64 类型 | int64 |
fixed32 | 定⻓ 4 字节。若值常⼤于228 则会⽐ uint32 更⾼效 | uint32 |
fixed64 | 定⻓ 8 字节。若值常⼤于228 则会⽐ uint32 更⾼效 | uint64 |
sfixed32 | 定⻓ 4 字节 | int32 |
sfixed64 | 定⻓ 8 字节 | int64 |
bool | bool | |
string | 包含 UTF-8 和 ASCII 编码的字符串,⻓度不能超过232 | string |
bytes | 可包含任意的字节序列但⻓度不能超过 232 | string |
[1] 变⻓编码是指:经过protobuf 编码后,原本4字节或8字节的数可能会被变为其他字节数。
此时我们就可以这样写:
syntax = "proto3";
package contacts;
message PeopleInfo
{string name = 1; int32 age = 2;
}
在这⾥还要特别讲解⼀下字段唯⼀编号
1 ~ 536,870,911 (229 - 1) ,其中 19000 ~ 19999 不可⽤。
9000 ~ 19999 不可⽤是因为:在 Protobuf 协议的实现中,对这些数进⾏了预留。如果⾮要在.proto
⽂件中使⽤这些预留标识号,例如将 name 字段的编号设置为19000,编译时就会报警。
值得⼀提的是,范围为 1 ~ 15 的字段编号需要⼀个字节进⾏编码, 16 ~ 2047 内的数字需要两个字节进⾏编码。编码后的字节不仅只包含了编号,还包含了字段类型。所以 1 ~ 15 要⽤来标记出现⾮常频繁的字段,要为将来有可能添加的、频繁出现的字段预留⼀些出来。
3 🍑编译 .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++ ⽂件。.
表示当前路径。OUT_DIR
编译后⽣成⽂件的⽬标路径。path/to/file.proto
要编译的.proto⽂件。
当我们编译成功后就会生成两个文件,一个头文件,一个源文件:
对于编译⽣成的 C++ 代码:
- 对于每个 message ,都会⽣成⼀个对应的消息类。
- 在消息类中,编译器为每个字段提供了获取和设置⽅法,以及⼀下其他能够操作字段的⽅法。
- 编辑器会针对于每个
.proto
⽂件⽣成.h
和.cc
⽂件,分别⽤来存放类的声明与类的实现。
我们在VSCode下观察 .h
文件:提示:有时候在查看时会出现大量飘红现象,这是由于插件的原因,本身是没有错误的。
- 每个字段都有设置和获取的⽅法,
get
的名称与⼩写字段完全相同,set
⽅法以set_
开头。 - 每个字段都有⼀个
clear_
⽅法,可以将字段重新设置回 empty 状态。
除此之外包括序列化⽅法和反序列化⽅法,这里列举小部分供参考:
class MessageLite
{
public://序列化: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);
};
注意:
- 序列化的结果为⼆进制字节序列,⽽⾮⽂本格式。
- 以上三种序列化的⽅法没有本质上的区别,只是序列化后输出的格式不同,可以供不同的应⽤场景使⽤。
- 序列化的 API 函数均为const成员函数,因为序列化不会改变类对象的内容, ⽽是将序列化的结果保存到函数⼊参指定的地址中。
查看更加详细的API点击这里:【API】
3 🍑序列化与反序列化的使用🍑
创建⼀个测试⽂件 main.cc,⽅法中我们实现:
- 对⼀个联系⼈的信息使⽤ PB 进⾏序列化,并将结果打印出来。
- 对序列化后的内容使⽤ PB 进⾏反序列,解析出联系⼈信息并打印出来。
参考代码:
#include <iostream>
#include "contacts.pb.h" // 引⼊编译⽣成的头⽂件
using namespace std;int main()
{string people_str;{contacts::PeopleInfo people;people.set_age(21);people.set_name("虾头男");// 调⽤序列化⽅法,将序列化后的⼆进制序列存⼊string中if (!people.SerializeToString(&people_str)){cout << "序列化联系⼈失败." << endl;}// 打印序列化结果cout << "序列化后的 people_str: " << people_str << endl;}{contacts::PeopleInfo people;// 调⽤反序列化⽅法,读取string中存放的⼆进制序列,并反序列化出对象if (!people.ParseFromString(people_str)){cout << "反序列化出联系⼈失败." << endl;}// 打印结果cout << "Parse age: " << people.age() << endl;cout << "Parse name: " << people.name() << endl;}
}
makefile:
test:main.ccg++ -o $@ $^ contacts.pb.cc -std=c++11 -lprotobuf
.PHONY:clean
clean:rm -r test
执行:
我们发现报了一个错误,原因是系统找不到共享库,我们执行下面命令即可:
sudo vim /etc/ld.so.conf
#添加以下路径
/usr/local/lib
修改/etc/ld.so.conf需要root权限。
然后执行:
sudo ldconfig
我们重新编译生成:
发现符合预期结果。由于序列化的结果是二进制,所以有些内容没有打印出来乱码很正常。
所以相对于 xml
和 JSON
来说,因为被编码成⼆进制,破解成本增⼤,ProtoBuf
编码是相对安全的。