C++序列化工具最佳实践
bigegpt 2025-01-08 11:20 6 浏览
序列化概述
当两个服务在进行通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以字节序列的形式在网络上发送。发送方需要把这个对象转换为字节序列,才能在网络上发送;接收方需要把字节序列再恢复为对象。
当服务上线后,将领域对象以字节序列的方式存储在分布式数据库中。当该服务突然宕机后,其上的既有业务迁移到了其他同类服务实例上,这时需要从数据库中获取字节序列反构领域对象,使得业务不中断。
这个把对象转换为字节序列的过程被称为“序列化”(serialization),而它的逆过程则被称为“反序列化” (deserialization)。这两个过程结合起来,可以在异构系统中轻松地存储和传输数据。
两种用途:
把对象的字节序列保存在文件或数据库中;
在网络上传送对象的字节序列。
必须序列化吗?
是的,核心问题是数据版本的前后项兼容,有了这个约束,就必须将对象序列化。
其他问题比如异构系统,虽然不是核心问题,但是序列化使得处理更加灵活。
C++序列化工具比较
对于通信系统,大多都是C/C++开发的,而C/C++语言没有反射机制,所以对象序列化的实现比较复杂,一般需要借助序列化工具。开源的序列化工具比较多,具体选择哪一个是受诸多因素约束的:
效率高;
前后向兼容性好;
支持异构系统;
稳定且被广泛使用;
接口友好;
...
下面我们比较几个常见的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);
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的最佳实践
对象序列化设计
序列化的单位为聚合或独立的实体,我们统一称为领域对象;
每个聚合可以引用其他聚合,序列化时将引用的对象指针存储为key,反序列化时根据key查询领域对象,将指针恢复为引用的领域对象的地址;
每个与序列化相关的类都要定义序列化和反序列化方法,可以通过通用的宏在头文件中声明,这样每个类只需关注本层的序列化,子对象的序列化由子对象来完成;
通过中间层来隔离protobuf对业务代码的污染,这个中间层暂时通过物理文件的分割来实现,即每个参与序列化的类都对应两个cpp文件,一个文件中专门用于实现序列化相关的方法,另一个文件中看不到protobuf的pb文件,序列化相关的cpp可以和领域相关cpp从目录隔离;
业务人员完成.proto文件的编写,Message结构要求简单稳定,数据对外扁平化呈现,一个领域对象对应一个.proto文件;
序列化过程可以看作是根据领域对象数据填充Message结构数据,反序列化过程则是根据Message结构数据填充领域对象数据;
领域对象的内部结构关系是不稳定的,比如重构,由于数据没变,所以不需要数据迁移;
当数据变了,同步修改.proto文件和序列化代码,不需要数据迁移;
当数据没变,但领域对象出现分裂或合并时,尽管概率很小,必须写数据迁移程序,而且要有数据迁移用例长期在CI运行,除非该用例对应的版本已不再维护;
服务宕机后,由其他服务接管既有业务,这时触发领域对象反构,反构过程包括反序列化过程,对业务是透明的。
对象序列化实战
假设有一个领域对象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++学习②】:http://jq.qq.com/?_wv=1027&k=2EMJ991
相关推荐
- 5分钟搭建公网https网页文件服务器,免费权威TLS证书
-
请关注本头条号,每天坚持更新原创干货技术文章。如需学习视频,请在微信搜索公众号“智传网优”直接开始自助视频学习前言本文主要讲解如何快速搭建一个https网页文件服务器,并免费申请权威机构颁发的tls证...
- nginx负载均衡配置(nginx负载均衡配置两个程序副本)
-
Nginx是什么没有听过Nginx?那么一定听过它的“同行”Apache吧!Nginx同Apache一样都是一种WEB服务器。基于REST架构风格,以统一资源描述符(UniformResources...
- 19《Nginx 入门教程》Nginx综合实践
-
今天我们将基于Nginx完成两个比较有用的场景,但是用到的Nginx的配置非常简单。内部Yum源搭建内部Pip源搭建1.实验环境ceph1centos7.6内网ip:172.16....
- Nginx性能调优与优化指南(nginx优化配置大全)
-
Nginx性能调优需要结合服务器硬件资源、业务场景和负载特征进行针对性优化。以下是一些关键优化方向和具体配置示例:一、Nginx配置优化1.进程与连接数优化nginxworker_process...
- C++后端开发必须彻底搞懂Nginx,从原理到实战(高级篇)
-
本文为Nginx实操高级篇。通过配置Nginx配置文件,实现正向代理、反向代理、负载均衡、Nginx缓存、动静分离和高可用Nginx6种功能,并对Nginx的原理作进一步的解析。当需...
- 【Nginx】史上最全的Nginx配置详解
-
Nginx服务器配置中最频繁的部分,代理、缓存和日志定义等绝大多数功能和第三方模块的配置都在这里,http块又包括http全局块和server块。Nginx是非常重要的负载均衡中间件,被广泛应用于大型...
- 【Nginx】Nginx 4种常见配置实例(nginx基本配置与参数说明)
-
本文主要介绍nginx4种常见的配置实例。Nginx实现反向代理;Nginx实现负载均衡;Nginx实现动静分离;Nginx实现高可用集群;Nginx4种常见配置实例如下:一、Nginx反向代理配...
- 使用nginx+allure管理自动化测试报告
-
allure在自动化测试中经常用来生成漂亮的报告,但是网上及官网上给出的例子都仅仅是针对单个测试用例文件的形式介绍的,实际使用中,自动化测试往往需要包含不止一个产品或项目,本文介绍如何使用nginx+...
- nginx配置文件详解(nginx配置文件详解高清版)
-
Nginx是一个强大的免费开源的HTTP服务器和反向代理服务器。在Web开发项目中,nginx常用作为静态文件服务器处理静态文件,并负责将动态请求转发至应用服务器(如Django,Flask,et...
- SpringCloud Eureka-服务注册与发现
-
1.Eureka介绍1.1学习Eureka前的说明目前主流的服务注册&发现的组件是Nacos,但是Eureka作为老牌经典的服务注册&发现技术还是有必要学习一下,原因:(1)一些早期的分布式微服...
- 微服务 Spring Cloud 实战 Eureka+Gateway+Feign+Hystrix
-
前言我所在项目组刚接到一个微服务改造需求,技术选型为SpringCloud,具体需求是把部分项目使用SpringCloud技术进行重构。本篇文章中介绍了Eureka、Gateway、Fe...
- 深度剖析 Spring Cloud Eureka 底层实现原理
-
你作为一名互联网大厂后端技术开发人员,在构建分布式系统时,是不是常常为服务的注册与发现而头疼?你是否好奇,像SpringCloudEureka这样被广泛使用的组件,它的底层实现原理到底是怎样的...
- 热爱生活,喜欢折腾。(很热爱生活)
-
原文是stackoverflow的一则高票回答,原文链接可能之前也有人翻译过,但是刚好自己也有疑惑,所以搬运一下,个人水平有限所以可能翻译存在误差,欢迎指正(如侵删)。尽管classmethod和st...
- GDB调试的高级技巧(详细描述gdb调试程序的全过程)
-
GDB是我们平时调试c/c++程序的利器,查起复杂的bug问题,比打印大法要好得多,但是也不得不说,gdb在默认情况下用起来并不是很好用,最近学习到几个高级点的技巧,分享下:一美化打印先上个例子...
- Arduino 实例(二十三)Arduino 给Python 编译器发送信息
-
1首先Python需要安装Pyserial库,在命令提示符中输入pipintallpyserial若是遇到提示‘pip‘不是内部或外部命令,也不是可运行的程序或批处理文件,则需要设置环境变...
- 一周热门
- 最近发表
- 标签列表
-
- mybatiscollection (79)
- mqtt服务器 (88)
- keyerror (78)
- c#map (65)
- resize函数 (64)
- xftp6 (83)
- bt搜索 (75)
- c#var (76)
- mybatis大于等于 (64)
- xcode-select (66)
- mysql授权 (74)
- 下载测试 (70)
- skip-name-resolve (63)
- linuxlink (65)
- pythonwget (67)
- logstashinput (65)
- hadoop端口 (65)
- vue阻止冒泡 (67)
- oracle时间戳转换日期 (64)
- jquery跨域 (68)
- php写入文件 (73)
- kafkatools (66)
- mysql导出数据库 (66)
- jquery鼠标移入移出 (71)
- 取小数点后两位的函数 (73)