序列化
简介
序列化主要用于需要持久化或通过网络传输的应用中。
序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
以网络传输为例,互联通讯的双方需要采用约定的协议,使用不同协议的机器之间即使有数据的传输,也无法实现正确的通信。就像一个说汉语的人和一个说巴勒斯坦语的人交流一样,即使他们都在说话,但由于互不理解意思,跟不说话的结果一样。
通讯协议往往采用分层模型,不同模型每层的功能定义以及颗粒度不同,例如:TCP/IP协议是一个四层协议,而OSI模型却是七层协议模型。序列化协议基本属于TCP/IP协议应用层的一部分。
有了序列化,发送者发送数据之前把数据序列化一下,接收者收到后反序列化一下,这样就实现了正确的通信。就像中国人与巴勒斯坦人交流一样,说话时先把自己语言转换为英语,听的人把英语再转化为自己的语言就可以了。
简而言之,以下情况需要序列化:
- 把内存中的对象状态保存到一个文件中或者数据库中时候;
- 用套接字在网络上传送对象的时候;
- 通过RMI传输对象的时候;
序列化是将数据结构或对象转换成二进制串的过程(存储在内存中的一块数据),反序列化是将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程
特性
- 通用性
- 技术层面,序列化协议是否支持跨平台、跨语言
- 流行程度,序列化和反序列化需要多方参与,很少人使用的协议往往意味着昂贵的学习成本
- 可调试性/可读性
- 性能
- 空间开销,序列化需要在原有的数据上加上描述字段,以为反序列化解析之用,这可能会导致过大的网络,磁盘等各方面的压力
- 时间开销,复杂的序列化协议会导致较长的解析时间,这可能会使得序列化和反序列化阶段成为整个系统的瓶颈
- 可扩展性/兼容性
- 支持自动增加新的业务字段,而不影响老的服务,这将大大提供系统的灵活度
- 安全性/访问限制
组件
- IDL(Interface description language)文件,参与通讯的各方需要对通讯的内容需要做相关的约定,为了建立一个与语言和平台无关的约定,这个约定需要采用与具体开发语言、平台无关的语言来进行描述。这种语言被称为接口描述语言(IDL),采用IDL撰写的协议约定称之为IDL文件
- IDL Compiler,IDL文件中约定的内容为了在各语言和平台可见,需要有一个编译器,将IDL文件转换成各语言对应的动态库
- Stub/Skeleton Lib,负责序列化和反序列化的工作代码
- Client/Server,应用层程序代码,他们面对的是IDL所生存的特定语言的class或struct
- 底层协议栈和互联网,序列化之后的数据通过底层的传输层、网络层、链路层以及物理层协议转换成数字信号在互联网中传递
常用的序列化工具
- XML是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点
- SOAP(Simple Object Access protocol) 是一种被广泛应用的,基于XML为序列化和反序列化协议的结构化消息传递协议
- JSON起源于弱类型语言Javascript,相对于XML而言,序列化后的数据更加简洁易读易理解,解析速度更快,具备Javascript的先天性支持,所以被广泛应用于Web browser的应用常景中,是Ajax的事实标准协议
- Protobuf具备了优秀的序列化协议的所需的众多典型特征,标准的IDL和IDL编译器,序列化数据非常简洁,紧凑,与XML相比,其序列化之后的数据量约为1/3到1/10,解析速度非常快,比对应的XML快约20-100倍
- FlatBuffers的功能和Protocol Buffers类似,最大不同点是在使用具体的数据之前,FlatBuffers不需要解析/解包的过程,在工程中使用时,FlatBuffers的引用比Protocol Buffers方便很多,只需要包含两三个头文件即可
- SBE(Simple Binary Encoding),提供低延迟高性能的二进制编码,灵感是来自于高性能工作组的 FIX Protocol Limited (FPL)组织,其最初目标是提供金融交易的低延迟的二进制编码
- Cap’n Proto,编码的数据格式跟在内存里面的布局是一致的,所以可以直接将编码好的structure直接字节存放到硬盘上面
后面三个都是速度“极快”的库,具体对比请参考 Cap’n Proto, FlatBuffers, and SBE 。
本文主要介绍FlatBuffers。
FlatBuffers
优缺点
同其他类似序列化工具相比,FlatBuffers的优缺点如下。
优点:
- 使用简单,定义协议文件(schema)、编译成源码、操作数据
- 效率高,数据在缓冲区内都是平整的,可以直接访问
- 使用便利,协议文件设计好后可以发布,可以随时往Table里面添加或者删除成员
缺点:
- 效率高的同时导致cpu资源占用相对较高
- 传输的数据可能比对数据进行压缩后再传输要大
- 打包后的FlatBuffers中的矢量数据不能随意修改(一般打包后直接发送,没有这个需求)
- FlatBuffers传输的数据没有JSON、XML和HTML那么直观
数据结构
-
struct
- Struct类型的数据结构是不可以更改的结构。当结构定义好,结构的成员、位置和大小就会固定,不能变更,否则所有的程序都要重新编译和升级。
- Struct的优点是访问速度快,占用内存少。
-
Table
- Table类型可以随意增加和删除成员,是一个很灵活的类型。当要删除一个成员时,把它指定为deprecated即可(为了支持向后美容,不允许删除Table中的字段)。
-
root_type
- 是一个顶级类型。
-
union
- 联合
FlatBuffers构造的数据总的来说是只要两种,即固定长度数据和可变长度数据,或者说是标量和矢量。
可以认为struct也是标量,因为struct的大小也是固定的。
在构造数据时,所有的标量都是直接写到缓冲区去的,所有的矢量需要先写到缓冲区,然后获得偏移量(32位),然后再把偏移量写到适当的结构中,每个矢量都离不开一个偏移量。
支持的类型
内置标量类型包括:
- 8 bit: byte (int8), ubyte (uint8), bool
- 16 bit: short (int16), ushort (uint16)
- 32 bit: int (int32), uint (uint32), float (float32)
- 64 bit: long (int64), ulong (uint64), double (float64)
括号内的名字是它前面类型的别名,两者使用方式等价。
内置非标量类型包括:
- 向量,使用[type]形式
- string,仅支持UTF-8或者7位ASCII码,其他编码或者通常的二进制数据可以使用向量[byte]/[ubyte]替代
- 引用其他tables/structs/enums/unions
使用
使用步骤
FlatBuffers的使用和 Protocol buffers 基本类似,主要步骤如下:
- 编写 schema 文件,描述数据结构和接口定义。
- 用 flatc 编译,生成相应语言的代码文件。
- 使用 FlatBuffers 支持的语言(如C ++,Java等)生成的文件进行开发。
fbs文件编写
根据程序中的结构体需求,编写fbs文件。
如下只使用了一个table:
// quote.fbs
namespace MD.quotation;
table QuoteData
{
type:int;
symbol:string;
highprice:double;
lowprice:double;
lastprice:double;
openprice:double;
precloseprice:double;
sellprice:[double];
sellvolume:[ulong];
buyprice:[double];
buyvolume:[ulong];
totalvolume:ulong;
time:string;
buylevelqueue:[uint];
selllevelqueue:[uint];
totalamount:double;
totalno:ulong;
iopv:double;
}
root_type QuoteData;
生成头文件
使用flatc生成头文件。
./flatc --cpp quote.fbs
编写程序
在对QuoteData进行序列化前,先对非标量进行序列化,包括string, vector, struct等。
测试程序如下:
#include <fstream>
#include <iostream>
#include "quote_generated.h" // 自动生成的头文件,注意把flatbuffers的头文件夹放在当前目录
using namespace MD::quotation;
using namespace std;
int encode_quote()
{
// 创建builder
flatbuffers::FlatBufferBuilder builder;
// 先序列化非标量
auto symbol = builder.CreateString("000025");
double sp[] = {0.23, 0.25, 0.27};
auto sellprice = builder.CreateVector(sp, 3);
unsigned long sv[] = {100, 1000, 10000};
auto sellvolume = builder.CreateVector(sv, 3);
double bp[] = {0.2, 0.21, 0.22};
auto buyprice = builder.CreateVector(bp, 3);
unsigned long bv[] = {100, 1000, 10000};
auto buyvolume = builder.CreateVector(bv, 3);
auto time = builder.CreateString("112556");
unsigned int bq[] = {1, 2, 3, 4};
auto buylevelqueue = builder.CreateVector(bq, 4);
unsigned int sq[] = {3, 4, 5, 6};
auto selllevelqueue = builder.CreateVector(sq, 4);
// 生成根节点
QuoteDataBuilder quote_builder(builder);
quote_builder.add_type(0);
quote_builder.add_symbol(symbol);
quote_builder.add_highprice(0.25);
quote_builder.add_lowprice(0.102);
quote_builder.add_lastprice(0.26);
quote_builder.add_openprice(2.22);
quote_builder.add_precloseprice(0.32);
quote_builder.add_sellprice(sellprice);
quote_builder.add_sellvolume(sellvolume);
quote_builder.add_buyprice(buyprice);
quote_builder.add_buyvolume(buyvolume);
quote_builder.add_totalvolume(23132);
quote_builder.add_time(time);
quote_builder.add_buylevelqueue(buylevelqueue);
quote_builder.add_selllevelqueue(selllevelqueue);
quote_builder.add_totalamount(4654654);
quote_builder.add_totalno(88);
quote_builder.add_iopv(20.25);
auto quote = quote_builder.Finish();
builder.Finish(quote);
// 得到序列化后的数据
const uint8_t* buf = builder.GetBufferPointer();
cout << "text size is: " << builder.GetSize() << endl;
// 写入文件,注意使用二进制格式,否则可能会出错
ofstream of;
of.open("a.txt", ios::out | ios::binary);
of.write((const char *)buf, builder.GetSize());
of.close();
return 0;
}
int decode_quote()
{
// 从文件中读取,注意使用二进制格式,否则会出错
ifstream ifile;
ifile.open("a.txt", ios::in | ios::binary);
char buf[9182];
ifile.read(buf, sizeof(buf));
ifile.close();
// 直接获取数据
uint8_t* bufp = (uint8_t*)buf;
auto quote = GetQuoteData(bufp);
cout << "type=" << quote->type() << endl;
cout << "symbol: " << quote->symbol()->c_str() << endl;
cout << "highprice: " << quote->highprice() << endl;
for (auto i = 0; i < quote->sellprice()->Length(); i++)
{
cout << "sellprice[" << i << "] = " << quote->sellprice()->Get(i) << ",";
cout << "sellvolume[" << i << "] = " << quote->sellvolume()->Get(i) << ",";
cout << "buyprice[" << i << "] = " << quote->buyprice()->Get(i) << ",";
cout << "buyvolume[" << i << "] = " << quote->buyvolume()->Get(i) << ",";
}
cout << endl;
return 0;
}
int main()
{
encode_quote();
decode_quote();
return 0;
}
使用c11编译运行,结果如下:
% ./a.out
text size is: 408
type=0
symbol: 000025
highprice: 0.25
sellprice[0] = 0.23,sellvolume[0] = 100,buyprice[0] = 0.2,buyvolume[0] = 100,sellprice[1] = 0.25,sellvolume[1] = 1000,buyprice[1] = 0.21,buyvolume[1] = 1000,sellprice[2] = 0.27,sellvolume[2] = 10000,buyprice[2] = 0.22,buyvolume[2] = 10000,
测试例程使用了简单的数据类型,基本演示了FlatBuffers的使用流程和方法。
更多具体内容可以参考官网教程 Tutorial 。