前端开发入门到精通的在线学习网站

网站首页 > 资源文章 正文

C++序列化工具最佳实践

qiguaw 2025-02-08 12:34:43 资源文章 13 ℃ 0 评论

序列化概述

当两个服务在进行通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以字节序列的形式在网络上发送。发送方需要把这个对象转换为字节序列,才能在网络上发送;接收方需要把字节序列再恢复为对象。

当服务上线后,将领域对象以字节序列的方式存储在分布式数据库中。当该服务突然宕机后,其上的既有业务迁移到了其他同类服务实例上,这时需要从数据库中获取字节序列反构领域对象,使得业务不中断。

这个把对象转换为字节序列的过程被称为“序列化”(serialization),而它的逆过程则被称为“反序列化” (deserialization)。这两个过程结合起来,可以在异构系统中轻松地存储和传输数据。

两种用途:

  1. 把对象的字节序列保存在文件或数据库中;

  2. 在网络上传送对象的字节序列。

必须序列化吗?

是的,核心问题是数据版本的前后项兼容,有了这个约束,就必须将对象序列化。

其他问题比如异构系统,虽然不是核心问题,但是序列化使得处理更加灵活。

C++序列化工具比较

对于通信系统,大多都是C/C++开发的,而C/C++语言没有反射机制,所以对象序列化的实现比较复杂,一般需要借助序列化工具。开源的序列化工具比较多,具体选择哪一个是受诸多因素约束的:

  1. 效率高;

  2. 前后向兼容性好;

  3. 支持异构系统;

  4. 稳定且被广泛使用;

  5. 接口友好;

  6. ...

下面我们比较几个常见的C++序列化工具。

msgpack是一个基于二进制的高效的对象序列化类库,可用于跨语言通信,号称比protobuf还要快4倍,但没有类似于optional的关键字,所以msgpack至少不满足前后项兼容的约束。

cereal是一个开源的(BSD License)、轻量级的、支持C++ 11特性的、仅仅包含头文件实现的、跨平台的C++序列化库。它可以将任意的数据类型序列化成不同的表现形式,比如二进制、XML格式或JSON。cereal的设计目标是快速、轻量级、易扩展——它没有外部的依赖关系,而且可以很容易的和其他代码封装在一块或者单独使用,但不能跨语言,所以cereal至少不满足异构系统系统的约束。

protobuf是一种轻便高效的结构化数据存储格式,可用于结构化数据串行化,很适合做数据存储或RPC数据交换格式。它可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。

在PC上单线程测试protobuf的性能结果如下:

单位数量
平均字节数35
序列化(1w次)时间(us)6803
反序列化(1w次)时间(us){{11952:0}}

通过表格来综合比较一下这三种序列化工具:

protobuf满足通信系统对序列化工具的选型约束,同时具有简单和高效的优点,所以protobuf比其他的序列化工具更具有吸引力。

protobuf C++使用指导

protobuf安装

在github上下载protobuf C++版本,并根据README.md的说明进行安装,此处不再赘述。

定义.proto文件

proto文件即消息协议原型定义文件,在该文件中我们可以通过使用描述性语言,来良好的定义我们程序中需要用到数据格式。

我们先通过一个电话簿的例子来了解下:

//AppExam.proto

syntax = "proto3";package App;message Person {

string name = 1; int32 id = 2;

string email = 3;

enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { required string number = 1; optional PhoneType type = 2 [default = HOME]; } repeated PhoneNumber phone = 4;}message AddressBook { repeated Person person = 1;}

正你看到的一样,消息格式定义很简单,对于每个字段而言可能有一个修饰符(repeated)、字段类型(bool/string/bytes/int32等)和字段标签(Tag)组成。

对于repeated的字段而言,该字段可以重复多个,即用于标记数组类型。

对于protobuf v2版本,除过repeated,还有required和optional,由于设计的不合理,在v3版本把这两个修饰符去掉了。

字段标签标示了字段在二进制流中存放的位置,这个是必须的,而且序列化与反序列化的时候相同的字段的Tag值必须对应,否则反序列化会出现意想不到的问题。

生成.h&.cc文件

进入protobuf的bin目录,输入命令:

./protoc -I=../../test/protobuf --cpp_out=../../test/protobuf ../../test/protobuf/AppExam.proto

I的值为.proto文件的目录,cpp_out的值为.h和.cc文件生成的目录,运行该命令后,在$cpp_out路径下生成了AppExam.pb.h和AppExam.pb.cc文件。

protobuf C++ API

生成的文件中有以下方法:

// name

inline bool has_name() const;

inline void clear_name();

inline const ::std::string& name() const;

inline void set_name(const ::std::string& value);

inline void set_name(const char* value);

inline ::std::string* mutable_name();

// id

inline bool has_id() const;

inline void clear_id();

inline int32_t id() const;

inline void set_id(int32_t value);

// email

inline bool has_email() const;

inline void clear_email();

inline const ::std::string& email() const;

inline void set_email(const ::std::string& value);

inline void set_email(const char* value);

inline ::std::string* mutable_email();

// phone

inline int phone_size() const;

inline void clear_phone();

inline const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phone() const;

inline ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phone();

inline const ::tutorial::Person_PhoneNumber& phone(int index) const;

inline ::tutorial::Person_PhoneNumber* mutable_phone(int index);

inline ::tutorial::Person_PhoneNumber* add_phone();

解析与序列化接口:

/* 序列化消息,将存储字节的以string方式输出,注意字节是二进制,而非文本;string!=text, serializes the message and stores the bytes in the given string. Note that the bytes are binary, not text; we only use the string class as a convenient container. */

bool SerializeToString(string* output) const;

//解析给定的string

bool ParseFromString(const string& data);

Any Message Type

protobuf在V3版本引入Any Message Type。

顾名思义,Any Message Type可以匹配任意的Message,包含Any类型的Message可以嵌套其他的Messages而不用包含它们的.proto文件。使用Any Message Type时,需要import文件google/protobuf/any.proto。

syntax = "proto3";package App;import "google/protobuf/any.proto";message ErrorStatus { repeated google.protobuf.Any details = 1;}message NetworkErrorDetails { int32 a = 1; int32 b = 2;}message LocalErrorDetails { int64 x = 1;

string y = 2;}

序列化时,通过pack操作将一个任意的Message存储到Any。

// Storing an arbitrary message type in Any.

App::NetworkErrorDetails details;details.set_a(1);details.set_b(2);App::ErrorStatus status;status.add_details()->PackFrom(details);

std::string str;status.SerializeToString(&str);

反序列化时,通过unpack操作从Any中读取一个任意的Message。

// Reading an arbitrary message from Any.

App::ErrorStatus status;

std::string str;status.ParseFromString(str);

for (const google::protobuf::Any& detail : status1.details()){ if (detail.Is()) { App::NetworkErrorDetails network_error; detail.UnpackTo(&network_error); INFO_LOG("NetworkErrorDetails: %d, %d", network_error.a(), network_error.b()); }}

protobuf的最佳实践

对象序列化设计

  1. 序列化的单位为聚合或独立的实体,我们统一称为领域对象;

  2. 每个聚合可以引用其他聚合,序列化时将引用的对象指针存储为key,反序列化时根据key查询领域对象,将指针恢复为引用的领域对象的地址;

  3. 每个与序列化相关的类都要定义序列化和反序列化方法,可以通过通用的宏在头文件中声明,这样每个类只需关注本层的序列化,子对象的序列化由子对象来完成;

  4. 通过中间层来隔离protobuf对业务代码的污染,这个中间层暂时通过物理文件的分割来实现,即每个参与序列化的类都对应两个cpp文件,一个文件中专门用于实现序列化相关的方法,另一个文件中看不到protobuf的pb文件,序列化相关的cpp可以和领域相关cpp从目录隔离;

  5. 业务人员完成.proto文件的编写,Message结构要求简单稳定,数据对外扁平化呈现,一个领域对象对应一个.proto文件;

  6. 序列化过程可以看作是根据领域对象数据填充Message结构数据,反序列化过程则是根据Message结构数据填充领域对象数据;

  7. 领域对象的内部结构关系是不稳定的,比如重构,由于数据没变,所以不需要数据迁移;

  8. 当数据变了,同步修改.proto文件和序列化代码,不需要数据迁移;

  9. 当数据没变,但领域对象出现分裂或合并时,尽管概率很小,必须写数据迁移程序,而且要有数据迁移用例长期在CI运行,除非该用例对应的版本已不再维护;

  10. 服务宕机后,由其他服务接管既有业务,这时触发领域对象反构,反构过程包括反序列化过程,对业务是透明的。

对象序列化实战

假设有一个领域对象Movie,有3个数据成员,分别是电影名字name、电影类型type和电影评分列表scores。Movie初始化时需要输入name和type,name输入后不能rename,可以看作Movie的key,而type输入后可以通过set来变更。scores是用户看完电影后的评分列表,而子项score也是一个对象,包括分值value和评论comment两个数据成员。

下面通过代码来说明电影对象的序列化和反序列化过程。

编写.proto文件

//AppObjSerializeExam.proto

syntax = "proto3";package App;message Score{ int32 value = 1;

string comment = 2;}message Movie{ string name = 1; int32 type = 2; repeated Score score = 3;}

领域对象的主要代码

序列化和反序列化接口是通用的,在每个序列化的类(包括成员对象所在的类)里面都要定义,因此定义一个宏,既增强了表达力又消除了重复。

// SerializationMacro.h

#define DECL_SERIALIZABLE_METHOD(T) \void serialize(T& t) const; \void deserialize(const T& t);

//MovieType.h

enum MovieType {HUMOR, SCIENCE, LOVE, OTHER};

//Score.h

namespace App{

struct Score;}

struct Score{ Score(U32 val = 0, std::string comment = "");

operator int() const; DECL_SERIALIZABLE_METHOD(App::Score);

private:

int value;

std::string comment;};

//Movie.h

typedef std::vector Scores;

const std::string UNKNOWN_NAME = "Unknown Name";

struct Movie{ Movie(const std::string& name = UNKNOWN_NAME, MovieType type = OTHER);

MovieType getType() const;

void setType(MovieType type);

void addScore(const Score& score);

BOOL hasScore() const;

const Scores& getScores() const; DECL_SERIALIZABLE_METHOD(std::string);

private:

std::string name; MovieType type; Scores scores;};

类Movie声明了序列化接口,而其数据成员scores对应的具体类Score也声明了序列化接口,这就是说序列化是一个递归的过程,一个类的序列化依赖于数据成员对应类的序列化。

序列化代码实现

首先通过物理隔离来减少依赖。

对于Score,有一个头文件Score.h,有两个实现文件Score.cpp和ScoreSerialization.cpp,其中ScoreSerialization.cpp为序列化代码实现文件。

//ScoreSerialization.cpp

void Score::serialize(App::Score& score) const

{ score.set_value(value); score.set_comment(comment);}

void Score::deserialize(const App::Score& score){ value = score.value(); comment = score.comment(); INFO_LOG("%d, %s", value, comment.c_str());}

同理,对于Movie,有一个头文件Movie.h,有两个实现文件Movie.cpp和MovieSerialization.cpp,其中MovieSerialization.cpp为序列化代码实现文件。

//MovieSerialization.cpp

void Movie::serialize(std::string& str) const

{

App::Movie movie; movie.set_name(name); movie.set_type(type); INFO_LOG("%d", scores.size());

for (size_t i = 0; i < scores.size(); i++) { App::Score* score = movie.add_score(); scores[i].serialize(*score); } movie.SerializeToString(&str);}

void Movie::deserialize(const std::string& str){ App::Movie movie; movie.ParseFromString(str); name = movie.name(), type = static_cast(movie.type()); U32 size = movie.score_size(); INFO_LOG("%s, %d, %d", name.c_str(), type, size); google::protobuf::RepeatedPtrField* scores = movie.mutable_score(); google::protobuf::RepeatedPtrField::iterator it = scores->begin(); for (; it != scores->end(); ++it) { Score score; score.deserialize(*it); addScore(score); }}

Any Message Type最佳实践

笔者对Any Message Type也进行了一定的实践,同时通过函数模板等方式提炼出了通用代码,但由于篇幅所限,本文不再展开。

小结

本文先介绍了序列化的基本概念和应用场景,并对常用的C++序列化工具进行了比较,发现protobuf比其他的序列化工具更具有吸引力,然后对protobuf C++的使用和特性进行了介绍,最后通过一个电影评分系统的案例展示了protobuf C++的经典实践,仅供参考,希望对大家有一定的价值。

想了解更多知识,一起学习交流共同进步点击链接加入群【C语言/C++学习②】:http://jq.qq.com/?_wv=1027&k=2EMJ991

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表