alin的学习之路:序列化与protobuf

1. 序列化(串行化)

序列化 (Serialization)将对象的状态信息转换为可以存储或传输的形式的过程,与之相对应的过程称之为反序列化(Unserialization)。序列化和反序列化主要用于解决在跨平台和跨语言的情况下, 模块之间的交互和调用,但其本质是为了解决数据传输问题

  • 序列化: 将数据(数据块(复合数据类型))变成字符串的过程
  • 数据块不能直接在网络环境中进行传输(字符串除外)
  • 反序列化: 将字符串还原为原始数据的过程
  • 序列化的应用场景:
  • 网络通信
  • 数据发送之前: 进行数据的序列化 -> 得到了字符串
  • 接收到数据之后: 进行数据的反序列化 -> 将字符串还原为原始数据
  • 数据的持久化存储: (持久化: 将数据从内存保存到磁盘的过程)
  • 在写文件的时候: 进行序列化
  • 文件内容读出之后: 进行反序列化
  • 这样做的目的是为了保存文件中数据的在网络传输/文件的的拷贝中不会出现解析错误

序列化的目的: 保证数据的跨平台传输

通俗来说:序列化就是将数据转换为字符串类型进行传递

1.1 为什么要用序列化

不使用序列化会发生如下的问题:

  1. 字节序的问题
  • 网络通信需要使用大端法,但是复合类型无法直接将整个类型进行转换,所以需要一个成员一个成员进行转换,十分繁琐
  1. 平台不同导致相同的数据类型所占内存大小不同
  • 32位
  • long :4字节
  • 64为
  • long :8字节
  1. 语言的差异导致相同的数据类型所占内存大小不同
  • java
  • char :2字节
  • C++
  • char :1字节
  1. 字节对齐
  • 字节的对齐是可以自行设置的,默认为4字节

1.2 常用的序列化方式

  1. Json( JavaScript Object Notation ) -> 是一种数据格式, 不是语言,轻量级的数据序列化的方式
  2. Protocol Buffer,谷歌
  3. boost 序列化的类,是C++的扩展库,不是标准C++

2. protobuf

使用protobuf3,要注意protobuf2和protobuf3不兼容

2.1 操作流程

  1. 准备数据: 非字符串类型的数据, 比如一个结构体, 或者一个类 --> 要被序列化的数据
  2. 创建一个新的文件, 文件名随意指定, 文件后缀为: .proto
  3. 根据protobuf的语法, 编辑 .proto 文件
  • 具体就指定要序列化的数据格式
  1. 通过可执行程序 protoc.exe 将编写好的 xxx.proto 文件进行转换 -> 得到两个c++的文件
  • 源文件: xxx.pb.cc --> xxx对应的名字和 .proto文件名相同
  • 头文件: xxx.pb.h --> xxx对应的名字和 .proto文件名相同
  1. 需要将生成的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编码的文本

bytes

string

处理多字节的语言字符、如中文, 建议protobuf中字符型类型使用 bytes

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;
}