搜公众号
推荐 原创 视频 Java开发 开发工具 Python开发 Kotlin开发 Ruby开发 .NET开发 服务器运维 开放平台 架构师 大数据 云计算 人工智能 开发语言 其它开发 iOS开发 前端开发 JavaScript开发 Android开发 PHP开发 数据库
Lambda在线 > 中兴开发者社区 > C++序列化工具最佳实践

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

中兴开发者社区 2018-05-29
举报



序列化概述


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


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


这个把对象转换为字节序列的过程被称为“序列化”(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


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




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>())   {      App::NetworkErrorDetails network_error;      detail.UnpackTo(&network_error);      INFO_LOG("NetworkErrorDetails: %d, %d", network_error.a(),             network_error.b());    } }



protobuf的最佳实践


对象序列化设计


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



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


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


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


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


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


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


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


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


对象序列化实战


假设有一个领域对象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<Score> 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<MovieType>(movie.type());    U32 size = movie.score_size();    INFO_LOG("%s, %d, %d", name.c_str(), type, size);    google::protobuf::RepeatedPtrField<App::Score>* scores =    movie.mutable_score();    google::protobuf::RepeatedPtrField<App::Score>::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++序列化工具最佳实践》的版权归原作者「中兴开发者社区」所有,文章言论观点不代表Lambda在线的观点, Lambda在线不承担任何法律责任。如需删除可联系QQ:516101458

文章来源: 阅读原文

相关阅读

举报