序列化:简单通⽤的数据交换格式JSON、MessagePack和ProtoBuffer
序列化,就是把内存⾥“活的对象”转换成静⽌的字节序列,便于存储和⽹络传输;⽽反序列化则是反向操作,从静⽌的字节序列重新构建出内存⾥可⽤的对象。
数据交换格式:JSON、MessagePack 和 ProtoBuffer。
对数据做序列化和反序列化:
JSON:
JSON 是⼀种轻量级的数据交换格式,采⽤纯⽂本表⽰,所以是“human readable”,阅读和修改都很⽅便。。第三⼯具:精选出序列化/ 反序列化、⽹络通信、脚本语⾔混合编程和性能分析这四类⼯具。
由于 JSON 起源于“最流⾏的脚本语⾔”JavaScript,所以它也随之得到了⼴泛的应⽤,在 Web 开发领域⼏乎已经成为了事实上的标准,⽽且还渗透到了其他的领域。⽐如很多数据库就⽀持直接存储 JSON 数据,还有很多应⽤服务使⽤ JSON 作为配置接⼝。在JSON 的官⽅⽹站上,你可以到⼤量的 C++ 实现,不过⽤起来都差不多。因为 JSON 本⾝就是个 KV 结构,很容易映射到类似 map 的关联数组操作⽅式。
JSON 格式注重的是⽅便易⽤,在性能上没有太⼤的优势,所以⼀般选择 JSON 来交换数据,通常都不会太在意性能(不然肯定会改换其他格式了),还是⾃⼰⽤着顺⼿最重要。
JSON for Modern C++ 可能不是最⼩最快的 JSON 解析⼯具,但功能⾜够完善,⽽且使⽤⽅便,仅需要包含⼀个头⽂件“json.hpp”,没有外部依赖,也不需要额外的安装、编译、链接⼯作,适合快速上⼿开发。JSON for Modern C++ 可以⽤“git clone”下载源码,或者更简单⼀点,直接⽤ wget 获取头⽂件就⾏:
git clone git@github:nlohmann/json.git # git clone
wget github/nlohmann/json/releases/download/v3.7.3/json.hpp # wget
JSON for Modern C++ 使⽤⼀个 json 类来表⽰ JSON 数据,为了避免说的时候弄混,我给这个类起了个别名 json_t:
using json_t = nlohmann::json;
json_t 的序列化功能很简单,和标准容器 map ⼀样,⽤关联数组的“[]”来添加任意数据。你不需要特别指定数据的类型,它会⾃动推导出恰当的类型。⽐如,连续多个“[]”就是嵌套对象,array、vector 或者花括号形式的初始化列表就是 JSON 数组,map 或者是花括号形式的 pair 就是 JSON 对象,⾮常⾃c语言单链表的基本操作
然:
json_t j;// JSON对象
j["age"]=23;// "age":23
j["name"]="spiderman";// "name":"spiderman"
j["gear"]["suits"]="2099";// "gear":{"suits":"2099"}
j["jobs"]={"superhero"};// "jobs":["superhero"]
vector<int> v ={1,2,3};// vector容器
j["numbers"]= v;// "numbers":[1,2,3]
map<string, int> m =// map容器
{{"one",1},{"two",2}};// 初始化列表
j["kv"]= m;// "kv":{"one":1,"two":2}
python解析json文件添加完之后,⽤成员函数 dump() 就可以序列化,得到它的 JSON ⽂本形式。默认的格式是紧凑输出,没有缩进,如果想要更容易阅读的话,可以加上指⽰缩进的参数:
cout << j.dump()<< endl;// 序列化,⽆缩进
cout << j.dump(2)<< endl;// 序列化,有缩进,2个空格
json_t 的反序列化功能同样也很简单,只要调⽤静态成员函数 parse() 就⾏,直接得到 JSON 对象,⽽且可以⽤ auto ⾃动推导类型:
string str =R"({// JSON⽂本,原始字符串
"name":"peter",
"age":23,
"married":true
})";
auto j = json_t::parse(str);// 从字符串反序列化
assert(j["age"]==23);// 验证序列化是否正确
assert(j["name"]=="peter");
json_t 使⽤异常来处理解析时可能发⽣的错误,如果你不能保证 JSON 数据的完整性,就要使⽤ try-catch 来保护代码,防⽌错误数据导致程序崩溃:
auto txt ="bad:data"s;// 不是正确的JSON数据
try// try保护代码
{
auto j = json_t::parse(txt);// 从字符串反序列化
}
catch(std::exception& e)// 捕获异常
{
cout << e.what()<< endl;
}
对于通常的应⽤来说,掌握了基本的序列化和反序列化就够⽤了,不过 JSON for Modern C++ ⾥还有很多⾼级⽤法,⽐如 SAX、BSON、⾃定义类型转换等。如果你需要这些功能,可以去看它的,⾥⾯写得都很详细。
MessagePack
说完 JSON,再来说另外第⼆种格式:MessagePack。
它也是⼀种轻量级的数据交换格式,与 JSON 的不同之处在于它不是纯⽂本,⽽是⼆进制。所以 MessagePack 就⽐ JSON 更⼩巧,处理起来更快,不过也就没有 JSON 那么直观、易读、好修改了。
由于⼆进制这个特点,MessagePack 也得到了⼴泛的应⽤,著名的有 Redis、Pinterest。MessagePack ⽀持⼏乎所有的编程语⾔,你可以在官⽹上到它的 C++ 实现。我常⽤的是官⽅库 msgpack-c,可以⽤ apt-get 直接安装。
apt-get install libmsgpack-dev
但这种安装⽅式有个问题,可能发⾏⽅仓库⾥的是⽼版本(像 Ubuntu 16.04 就是 0.57),缺失很多功能,所以最好是从GitHub上下载最新版,编译时⼿动指定包含路径:
git clone git@github:msgpack/msgpack-c.git
g++ msgpack.cpp -std=c++14-I../common/include -o a.out
和 JSON for Modern C++ ⼀样,msgpack-c 也是仅头⽂件的库(head only),只要包含⼀个头⽂件“msgpack.hpp”就⾏了,不需要额外的编译链接选项(C 版本需要⽤“-lmsgpackc”链接)。
但 MessagePack 的设计理念和 JSON 是完全不同的,它没有定义 JSON 那样的数据结构,⽽是⽐较底层,只能对基本类型和标准容器序列化 / 反序列化,需要你⾃⼰去组织、整理要序列化的数据。
我拿 vector 容器来举个例⼦,调⽤ pack() 函数序列化为 MessagePack 格式:
vector<int> v ={1,2,3,4,5};// vector容器
msgpack::sbuffer sbuf;// 输出缓冲区
msgpack::pack(sbuf, v);// 序列化
从代码⾥你可以看到,它的⽤法不像 JSON 那么简单直观,必须同时传递序列化的输出⽬标和被序列化的对象。
输出⽬标 sbuffer 是个简单的缓冲区,你可以把它理解成是对字符串数组的封装,和vector很像,也可以⽤ data() 和 size() ⽅法获取内部的数据和长度。
cout << sbuf.size()<< endl;// 查看序列化后数据的长度
除了 sbuffer,你还可以选择另外的 zbuffer、fbuffer。它们是压缩输出和⽂件输出,和 sbuffer 只是格式不同,⽤法是相同的,所以后⾯我就都⽤ sbuffer 来举例说明。
MessagePack 反序列化的时候略微⿇烦⼀些,要⽤到函数 unpack() 和两个核⼼类:object_handle 和 object。
函数 unpack() 反序列化数据,得到的是⼀个 object_handle,再调⽤ get(),就是 object:
auto handle = msgpack::unpack(// 反序列化
sbuf.data(), sbuf.size());// 输⼊⼆进制数据
auto obj = ();// 得到反序列化对象
这个 object 是 MessagePack 对数据的封装,相当于 JSON for Modern C++ 的 JSON 对象,但你不能直接使⽤,必须知道数据的原始类型,才能转换还原:
vector<int> v2;// vector容器
assert(std::equal(// 算法⽐较两个容器
begin(v),end(v),begin(v2)));
因为 MessagePack 不能直接打包复杂数据,所以⽤起来就⽐ JSON ⿇烦⼀些,你必须⾃⼰把数据逐个序列化,连在⼀起才⾏。
好在 MessagePack ⼜提供了⼀个 packer 类,可以实现串联的序列化操作,简化代码:
msgpack::sbuffer sbuf;// 输出缓冲区
msgpack::packer<decltype(sbuf)>packer(sbuf);// 专门的序列化对象
packer.pack(10).pack("monado"s)// 连续序列化多个数据
.pack(vector<int>{1,2,3});
对于多个对象连续序列化后的数据,反序列化的时候可以⽤⼀个偏移量(offset)参数来同样连续操作:
for(decltype(sbuf.size()) offset =0;// 初始偏移量是0
offset != sbuf.size();){// 直⾄反序列化结束
auto handle = msgpack::unpack(// 反序列化
sbuf.data(), sbuf.size(), offset);// 输⼊⼆进制数据和偏移量java模拟器电脑版下载
auto obj = ();// 得到反序列化对象
}
但这样还是⽐较⿇烦,能不能像 JSON 那样,直接对类型序列化和反序列化呢?MessagePack 为此提供了⼀个特别的宏:
MSGPACK_DEFINE,把它放进你的类定义⾥,就可以像标准类型⼀样被 MessagePack 处理。
下⾯定义了⼀个简单的 Book 类:
class Book final // ⾃定义类
{
public:
int id;
string title;
set<string> tags;
public:
MSGPACK_DEFINE(id, title, tags);// 实现序列化功能的宏
};
它可以直接⽤于 pack() 和 unpack(),基本上和 JSON 差不多了:
Book book1 ={1,"1984",{"a","b"}};// ⾃定义类
msgpack::sbuffer sbuf;// 输出缓冲区
msgpack::pack(sbuf, book1);// 序列化
auto obj = msgpack::unpack(// 反序列化
sbuf.data(), sbuf.size()).get();// 得到反序列化对象
Book book2;
assert(book2.id == book1.id);
assert(book2.tags.size()==2);
cout << book2.title << endl;
使⽤ MessagePack 的时候,你也要注意数据不完整的问题,必须要⽤ try-catch 来保护代码,捕获异常:
auto txt =""s;// 空数据
try// try保护代码
{
auto handle = msgpack::unpack(// 反序列化
txt.data(), txt.size());
}
catch(std::exception& e)// 捕获异常
{
cout << e.what()<< endl;
}
ProtoBuffer
apt-get install protobuf-compiler
apt-get install libprotobuf-dev
g++ protobuf.cpp -std=c++14-lprotobuf -o a.out
PB 的另⼀个特点是数据有“模式”(schema),必须要先写⼀个 IDL(Interface Description Language)⽂件,在⾥⾯定义好数据结构,只有预先定义了的数据结构,才能被序列化和反序列化。
这个特点既有好处也有坏处:⼀⽅⾯,接⼝就是清晰明确的规范⽂档,沟通交流简单⽆歧义;⽽另⼀⽅⾯,就是缺乏灵活性,改接⼝会导致⼀连串的操作,有点繁琐。
下⾯是⼀个简单的 PB 定义:
syntax ="proto2";// 使⽤第2版
package sample;// 定义名字空间
message Vendor // 定义消息
{
required uint32 id =1;// required表⽰必须字段
required string name =2;// 有int32/string等基本类型
required bool valid =3;// 需要指定字段的序号,序列化时⽤
optional string tel =4;// optional字段可以没有
}
有了接⼝定义⽂件,需要再⽤ protoc ⼯具⽣成对应的 C++ 源码,然后把源码⽂件加⼊⾃⼰的项⽬中,就可以使⽤了:
protoc --cpp_out=. sample.proto // ⽣成C++代码
由于 PB 相关的资料实在太多了,这⾥我就只简单说⼀下重要的接⼝:
字段名会⽣成对应的 has/set 函数,检查是否存在和设置值;
IsInitialized() 检查数据是否完整(required 字段必须有值);
DebugString() 输出数据的可读字符串描述;
ByteSize() 返回序列化数据的长度;
SerializeToString() 从对象序列化到字符串;
ParseFromString() 从字符串反序列化到对象;
SerializeToArray()/ParseFromArray() 序列化的⽬标是字节数组。
下⾯的代码⽰范了 PB 的⽤法:
using vendor_t = sample::Vendor;// 类型别名
vendor_t v;// 声明⼀个PB对象
assert(!v.IsInitialized());// required等字段未初始化
v.set_id(1);// 设置每个字段的值
v.set_name("sony");
v.set_valid(true);
assert(v.IsInitialized());// required等字段都设置了,数据完整
assert(v.has_id()&& v.id()==1);
assert(v.has_name()&& v.name()=="sony");
assert(v.has_valid()&& v.valid());
cout << v.DebugString()<< endl;// 输出调试字符串
string enc;
for循环先判断还是先执行v.SerializeToString(&enc);// 序列化到字符串
vendor_t v2;
assert(!v2.IsInitialized());
v2.ParseFromString(enc);// 反序列化
虽然业界很多⼤⼚都在使⽤ PB,但我觉得它真不能算是最好的,IDL 定义和接⼝都太死板⽣硬,还只能⽤最基本的数据类型,不⽀持标准容器,在现代 C++ ⾥显得“不太合”,⽤起来有点别扭。
不过它后⾯有 Google“撑腰”,⽽且最近⼏年⼜有 gRPC“助拳”,所以很多时候也不得不⽤。
PB 的另⼀个缺点是官⽅⽀持的编程语⾔太少,通⽤性较差,最常⽤的 proto2 只有 C++、Java 和 Python。后来的 proto3 增加了对Go、Ruby 等的⽀持,但仍然不能和 JSON、MessagePack 相⽐。
这三种数据格式各有特⾊,在很多领域都得到了⼴泛的应⽤,简单⼩结⼀下:
JSON 是纯⽂本,容易阅读,⽅便编辑,适⽤性最⼴;
MessagePack 是⼆进制,⼩巧⾼效,在开源界接受程度⽐较⾼;
ProtoBuffer 是⼯业级的数据格式,注重安全和性能,多⽤在⼤公司的商业产品⾥。
常量的数据类型有几种有很多开源库⽀持这些数据格式,官⽅的、民间的都有,你应该选择适合⾃⼰的⾼质量库,必要的时候可以做些测试。
再补充⼀点,还有知名的有 Avro、Thrift,虽然它们有点冷门,但也有⾃⼰的独到之处(⽐如,天⽣⽀持 RPC、可选择多种序列化格式和传输⽅式)。
为什么要有序列化和反序列化,直接 memcpy 内存数据⾏不⾏呢?
你最常⽤的是哪种数据格式?它有什么优缺点?
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论