alin的学习之路:序列化与protobuf
1. 序列化(串行化)
序列化 (Serialization)将对象的状态信息转换为可以存储或传输的形式的过程,与之相对应的过程称之为反序列化(Unserialization)。序列化和反序列化主要用于解决在跨平台和跨语言的情况下, 模块之间的交互和调用,但其本质是为了解决数据传输问题。
- 序列化: 将数据(
数据块(复合数据类型)
)变成字符串的过程- 数据块不能直接在网络环境中进行传输(
字符串除外
)- 反序列化:
将字符串还原为原始数据的过程
- 序列化的应用场景:
- 网络通信
- 数据发送之前: 进行数据的序列化 -> 得到了字符串
- 接收到数据之后: 进行数据的反序列化 -> 将字符串还原为原始数据
- 数据的持久化存储: (
持久化: 将数据从内存保存到磁盘的过程
)
- 在写文件的时候: 进行序列化
- 文件内容读出之后: 进行反序列化
这样做的目的是为了保存文件中数据的在网络传输/文件的的拷贝中不会出现解析错误
序列化的目的: 保证数据的跨平台传输
通俗来说:序列化就是将数据转换为字符串类型进行传递
1.1 为什么要用序列化
不使用序列化会发生如下的问题:
- 字节序的问题
- 网络通信需要使用大端法,但是复合类型无法直接将整个类型进行转换,所以需要一个成员一个成员进行转换,十分繁琐
- 平台不同导致相同的数据类型所占内存大小不同
- 32位
- long :4字节
- 64为
- long :8字节
- 语言的差异导致相同的数据类型所占内存大小不同
- java
- char :2字节
- C++
- char :1字节
- 字节对齐
- 字节的对齐是可以自行设置的,默认为4字节
1.2 常用的序列化方式
- Json( JavaScript Object Notation ) -> 是一种数据格式, 不是语言,轻量级的数据序列化的方式
- Protocol Buffer,谷歌
- boost 序列化的类,是C++的扩展库,不是标准C++
2. protobuf
使用protobuf3,要注意protobuf2和protobuf3不兼容
2.1 操作流程
- 准备数据: 非字符串类型的数据, 比如一个结构体, 或者一个类 --> 要被序列化的数据
- 创建一个新的文件, 文件名随意指定, 文件后缀为: .proto
- 根据protobuf的语法, 编辑 .proto 文件
- 具体就指定要序列化的数据格式
- 通过可执行程序 protoc.exe 将编写好的 xxx.proto 文件进行转换 -> 得到两个c++的文件
- 源文件: xxx.pb.cc --> xxx对应的名字和 .proto文件名相同
- 头文件: xxx.pb.h --> xxx对应的名字和 .proto文件名相同
- 需要将生成的c++文件添加到项目中, 通过文件中提供的类实现数据的序列化/反序列化
2.2 .proto文件及其使用
// 要序列化的数据
// 第一种:
int number;
// 第二种:
struct Person
{
int id;
string name;
string sex; // man woman
int age;
};
protobuf中的数据类型 和 C++ 数据类型对照表:
.proto类型 | **C++**类型 | 备注 |
double | double | 64位浮点数 |
float | float | 32位浮点数 |
int32 | int (32位) | 32位整数 |
int64 | long (64位) | 64位整数 |
uint32 | unsigned int(32位) | 32位无符号整数 |
uint64 | unsigned long(64位) | 64位无符号整数 |
sint32 | signed int(32位) | 32位整数,处理负数效率比int32更高 |
sint64 | signed long(64位) | 64位整数,处理负数效率比int64更高 |
fixed32 | unsigned int(32位) | 总是4个字节。如果数值总是比总是比228大的话,这个类型会比uint32高效。 |
fixed64 | unsigned long(64位) | 总是8个字节。如果数值总是比总是比256大的话,这个类型会比uint64高效。 |
sfixed32 | int (32位) | 总是4个字节 |
sfixed64 | long (64位) | 总是8个字节 |
bool | bool | 布尔类型 |
string | string | 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本 |
| string | 处理多字节的语言字符、如中文, 建议protobuf中字符型类型使用 |
enum | enum | 枚举 |
message | object of class | 自定义的消息类型 |
- proto文件的语法
//第一行指定protobuf的版本,注意这一行必须和下面写的一模一样,包括空格,否则会有警告
syntax = "proto3";
//定义要序列化的数据格式
//message 对应 struct 或者 class
message 名字 //这个名字就是类名
{
数据类型 成员名 = 编号; //编号要从1开始,每个成员都有唯一的编号,不能重复,递增即可
}
// message末尾可以不写结束的分号,写了也不报错
举个栗子:
syntax = "proto3";
//第一种,复合数据类型
message Person
{
int32 id = 1;
bytes name = 2;
bytes sex = 3;
int32 age = 4;
}
//第二种,单独一个变量
//int number; 这样一个单独的数据块,要进行序列化也必须放入一个类中
message Data
{
int32 number = 1;
}
- 将 .proto文件编译转换为C++文件、
# 为了更方便的进行 protoc.exe 的全局访问, 需要将其所在目录设置到环境变量中
# 如果环境变量设置好了, 使用 protoc.exe 可以直接写全名也可以写成 protoc
# 意味着: protoc == protoc.exe
# 语法格式: [在命令行窗口中执行这个命令]
# .proto文件的名字需要携带路径, 保证可以找到这个文件(绝对路径/相对路径)
$ protoc proto文件的名字 --cpp_out=输出C++文件的路径
# 举个栗子
$ protoc ./person.proto --cpp_out=.
- 粗略的阅读生成的头文件
**.pb.h
- 通过
protoc.exe
的转换得到的 头文件中有一个类, 这个类的名字和.proto
文件中message关键字后边指定的名字相同
, .proto文件中的成员就是生成的类的私有成员 - 如果访问生成的类的私有成员呢? —> 调用提供的公共成员函数 --> 这个函数的规律:
- 清空(初始化) 私有成员的值:
clear_变量名()
- 获取类私有成员的值(
使用同名函数
):变量名()
- 给私有成员进行值的设置:
set_变量名(参数)
- 得到类私有成员的
地址
, 通过这块地址读/写当前私有成员变量的值
:mutable_变量名()
- 如果这个变量是数组类型:
- 数组中元素的个数:
变量名_size()
- 添加一块内存, 存储新的元素数据:
add_变量名()
- probufbuf 提供的常用的序列化/反序列化的 API,需要加载动态库
libprotobuf.dll
:
以下函数都是被protobuf中的某一个对象调用的,
- 序列化:
序列化的数据来自于这个对象, 序列化完成之后, 保存到一块内存中, 这块内存不属于protoubf对象
- 反序列化:
将外部数据还原, 还原到这个protobuf对象中存储起来, 访问protoubf对象就可以得到原始数据
- 序列化
// 头文件目录: google\protobuf\message_lite.h
// --- 将序列化的数据 数据保存到内存中
// 将类对象中的数据序列化为字符串, c++ 风格的字符串, 参数是一个传出参数
bool SerializeToString(std::string* output) const;
// 将类对象中的数据序列化为字符串, c 风格的字符串, 参数 data 是一个传出参数
bool SerializeToArray(void* data, int size) const;
// ------ 写磁盘文件, 只需要调用这个函数, 数据自动被写入到磁盘文件中
// -- 需要提供流对象/文件描述符关联一个磁盘文件
// 将数据序列化写入到磁盘文件中, c++ 风格
// ostream 子类 ofstream -> 写文件
bool SerializeToOstream(std::ostream* output) const;
// 将数据序列化写入到磁盘文件中, c 风格
bool SerializeToFileDescriptor(int file_descriptor) const;
- 反序列化的api
// 头文件目录: google\protobuf\message_lite.h
bool ParseFromString(const std::string& data) ;
bool ParseFromArray(const void* data, int size);
// istream -> 子类 ifstream -> 读操作
// wo ri
// w->写 o: ofstream , r->读 i: ifstream
bool ParseFromIstream(std::istream* input);
bool ParseFromFileDescriptor(int file_descriptor);
- 注意如果使用Qt进行开发时,需要在.pro文件中指定好protobuf的头文件的路径和动态库的路径
INCLUDEPATH += C:\protobuf\include
LIBS += -LC:\protobuf\lib -lprotobuf
2.3 repeated 限定修饰符
// 要序列化的数据
struct Person
{
int id;
string name[10];
string sex;
int age;
};
// 对要序列化的数据进行转换
// person.proto
// 在该文件中对要序列化的结构体进行描述
message Person
{
int32 id = 1;
repeated bytes name = 2; // 这个name可以作为一个动态数组来使用
bytes sex = 3;
int32 age = 4;
}
- 每次给这个数组设置值之前都需要提前开辟一块空间,使用
add_变量名
,然后再set_变量名
,反序列化后调用其中的数据也需要加上参数。总之:注意参数
2.4 枚举
在写c++程序的时候, 枚举的使用频率是很高的, 在c++中一般建议使用枚举将宏替换掉
// 要序列化的数据
// 枚举
enum Color
{
Red = 5, // 可以不给初始值, 默认为0
Green,
Yellow,
Blue
};
// 要序列化的数据
struct Person
{
int id;
string name[10];
string sex;
int age;
// 枚举类型
Color color;
};
// 在protobuf的proto文件中处理枚举类型
// 语法格式: 元素之间使用 分号间隔 ;
enum 名字
{
元素名 = 0; // 枚举中第一个原素的值必须为0
元素名 = x;
}
如何修改 /
// 定义枚举类型
enum Color
{
Red = 0; // 第一个元素的枚举值必须为0,并且必须指定出来
Green = 3; // 其余的元素也要显示的指定出值
Yellow = 6;
Blue = 9;
}
// 在该文件中对要序列化的结构体进行描述
message Person
{
int32 id = 1;
repeated bytes name = 2;
bytes sex = 3;
int32 age = 4;
// 枚举类型
Color color = 5;
}
2.5 proto文件的导入
假设说 有多个 .proto文件, 每个proto文件中对应一个
message
也就是对应一个类的定义, 如果这几个类直接需要相互引用, 比如:在类A中包含一个B类型的成员
, 这时候需要在一个 proto文件引入另外一个proto文件
- proto文件 - address.proto
syntax = "proto3";
// 地址信息
message Address
{
bytes addr = 1;
bytes number = 2;
}
- proto文件 - person.proto
// 现在的需求: 需要在当前的proto文件中使用一个proto文件中定义的数据
// 需要进行proto文件的包含
// 语法: import "要使用的proto文件的名字";
/// 使用 ///
syntax = "proto3";
// 使用另外一个proto文件中的数类型, 需要导入这个文件
import "address.proto";
// 在该文件中对要序列化的结构体进行描述
// 定义枚举类型
enum Color
{
Red = 0;
Green = 3; // 第一个元素以外的元素值可以随意指定
Yellow = 6;
Blue = 9;
}
// 在该文件中对要序列化的结构体进行描述
message Person
{
int32 id = 1;
repeated bytes name = 2;
bytes sex = 3;
int32 age = 4;
// 枚举类型
Color color = 5;
// 添加地址信息, 使用的是外部proto文件中定义的数据类型
Address addr = 6;
}
- 注意在代码中进行使用的时候,要先得到得到类私有成员的
地址
, 通过这块地址读/写当前私有成员变量的值
:mutable_变量名()
mutable_变量名()
返回的是一个对应对象的指针,给其中的元素设置值等操作是要使用这个指针来调用函数来操作。
2.6 包
这个包在c++中对应的是
命名空间
, 作用是用来隔离数据
- 比如做项目开发, A编写了一类:
HelloWorld
, B也写了一个类:HelloWorld
, 但是这两个人写的类虽然同名, 但是功能不一样, 直接使用是冲突的, 可以将这两个类放到不同的命名空间中就可以了
// 如果要给生成的c++类指定命名空间, 语法格式:
package 命名空间的名字;
- proto文件 - Address.proto
syntax = "proto3";
// 添加命名空间 itcast
package itcast;
// 地址信息, 这个Address类属于命名空间: itcast
message Address
{
bytes addr = 1;
bytes number = 2;
}
- proto文件 - person.proto
syntax = "proto3";
// 使用另外一个proto文件中的数类型, 需要导入这个文件
import "address.proto";
// 指定命名空间 itheima
package itheima;
// 一下的类 Person 和枚举 Color 都属于命名空间 itheima
// 在该文件中对要序列化的结构体进行描述
// 定义枚举类型
enum Color
{
Red = 0;
Green = 3; // 第一个元素以外的元素值可以随意指定
Yellow = 6;
Blue = 9;
}
// 在该文件中对要序列化的结构体进行描述
message Person
{
int32 id = 1;
repeated bytes name = 2;
bytes sex = 3;
int32 age = 4;
// 枚举类型
Color color = 5;
// 添加地址信息, 使用的是外部proto文件中定义的数据类型
// 如果这个外边类型属于某个命名空间, 语法格式:
// 命名空间的名字.类名 变量名=编号;
itcast.Address addr = 6;
}
- 添加完命名空间后,在程序中调用对象需要加上
命名空间::
或者是加上using namespace 命名空间;
注意:在.proto文件中调用其他命名空间中的对象时要写上 命名空间.对象名
2.7 实例代码
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "person.pb.h"
#include "address.pb.h"
#include <qdebug.h>
using namespace std;
#if 0
using namespace itcast;
using namespace itheima;
#endif
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
//创建对象
itcast::Person p;
//创建Address的指针
itheima::Address *addr = p.mutable_addr();
addr->set_addr("天津市东丽区");
addr->set_number("10-10-501");
//设置值
p.set_id(10);
//给name数组设置值
p.add_name();
p.set_name(0, "路飞");
p.add_name();
p.set_name(1, "乔巴");
p.add_name();
p.set_name(2, "索隆");
p.set_sex("男");
p.set_age(18);
//设置枚举的值
p.set_color(itcast::Color::Green);
//序列化
string output;
p.SerializeToString(&output);
//反序列化
itcast::Person pp;
pp.ParseFromString(output);
//得到Address的对象
itheima::Address addr1 = pp.addr();
qDebug() << "id:" << pp.id()
<< "name1:" << pp.name(0).data()
<< "name1:" << pp.name(1).data()
<< "name1:" << pp.name(2).data()
<< "sex:" << pp.sex().data()
<< "age:" << pp.age()
<< "color:" << pp.color() << endl
<< "addr addr:" << addr1.addr().data()
<< "addr number:" << addr1.number().data();
}
MainWindow::~MainWindow()
{
delete ui;
}