大型分布式C++框架《三:序列化与反序列化》

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 一、前言      个人感觉序列化简单来说就是按一定规则组包。反序列化就是按组包时的规则来接包。正常来说。序列化不会很难。不会很复杂。因为过于复杂的序列化协议会导致较长的解析时间,这可能会使得序列化和反序列化阶段成为整个系统的瓶颈。

一、前言 

 


    个人感觉序列化简单来说就是按一定规则组包。反序列化就是按组包时的规则来接包。正常来说。序列化不会很难。不会很复杂。因为过于复杂的序列化协议会导致较长的解析时间,这可能会使得序列化和反序列化阶段成为整个系统的瓶颈。就像压缩文件、解压文件,会占用大量cpu时间。

    所以正常的序列化会在时间和空间上考虑。个人感觉对于电商业务时间应该是相对重要些。毕竟用户没有那么多时间等你解析。
 
           我们是用thrift来序列化的。一份thrift文件生成2份。一份是c++生成的用来编写服务接口。一份是php生成的。所有请求都会先落到前端机器。然后前端机器用php调用服务端函数接口。返回处理结果。这其实是远程调用rpc。
 

二、分配序列化空间的大小


说序列化之前先说下平台给序列化分配的buf的空间大小

1、每个协程会分配大概固定包头(56个字节)+特殊buf(200个字节)的空间来保存包头。所以首先如果收到的包特殊buf(就是放sessionkey和uid等信息)大于200个字节。会报错不处理。但是并不会给netio 返回一个错误包消息。所以客户端 会一直等到客户端设置的超时时间
2、每个container会分配3M的空间来处理数据。所以去掉包头和特殊buf.剩下的就是可以用来序列化的空间 3*1024*1024-固定包头-特殊buf。   所以最少会有 3*1024*1024-56-200的空间
        这里其实可以看到协程的好处。这个3M的空间。对于每个协程来说是共享的。因为我们是协程的方式,其实是一种顺序流程,没有协程会跟你竞争使用这个buf的资源。因为可以自己手动控制协程的切换。
        如果是多线程的话。可能就要对这个buf加锁。竞争这一个全局资源来处理数据。这也是多线程编程被诟病的一个地方,需要加锁。
 
 

三、序列化步骤

 


1、我们先看下请求。

复制代码
    oCntlInfo.setOperatorUin(10458);
    oCntlInfo.setOperatorKey("abcde");
    oCntlInfo.setRouteKey(1234);
 
    std::string source = "aaaaa";
    std::string inReserve;
    std::string errmsg;
    std::string outStr;
 
 
    std::string machineKey;
    for(int i =0;i<500*1024;i++)
    {
        machineKey.append("a");
    }
 
 
    AoActionInfo oActionInfo;
    oActionInfo.SetDisShopId(1111);
    oActionInfo.SetDistributorId(2222);
 
    uint32_t dwResult = Stub4App.AddActionSupplier(
                oCntlInfo,
                machineKey,
                source,
                1,
                1,
                oActionInfo,
                inReserve,
                errmsg,
                outStr);
    if(dwResult == 0)
    {
        std::cout << "Invoke OK!" << std::endl;
        std::cout << "Invoke OK!" << std::endl;
 
    }
复制代码

客户端直接调用函数接口。到服务端请求结果

最后需要序列化的东西如下是类_Cao_action_AddActionSupplier_Req

函数的入参都是我们需要序列化的内容。注意这里是rpc调用的一个关键点。

2、序列化开始

a)    先看下我们的thritf

如果下图。发现我们的函数入参也是打上了tag标志的。作用跟我们在结构体中打tag标志是一样的。为了标识一个字段的含义。

序列化的时候把这些tag序列化进去。 然后反序列化的时候靠这些tag来解析

 

b ) 先把图贴出来。按着图来讲更清晰些

 

 

c)   首先我们会创建一个CByteStream的类来。序列化内容。在CByteStream的构造函数会自动写入一个字节的序列化包头。值为1
CByteStream(char* pStreamBuf = NULL, uint32_t nBufLen = 0,bool bStore=truebool bRealWrite = true);
pStreamBuf  是序列化buf指针
pStreamBuf  是序列化的长度
bStore  true表示是否需要包数据存储下来。  false表示不需要把数据存下来
bRealWrite 表示是否支持读写buf
d)   接着就开始写类_Cao_action_AddActionSupplier_Req的成员变量。其实就是函数入参。写的时候是先协tag就是下图中的fid。  其实就是在thrift中已经写好的函数入参的tag值。
具体写的过程我们先看简单基本类型。比如strMachineKey
      1)先写tag。  strMachineKey 的tag为1.  程序里规定tag占两个字节。所以函数入参可以是0xffff个。
     2 ) 接着会写4个固定的字节。用来存储后面紧跟着数据的值。这里strMachineKey的长度是512000.
     3 ) 写内容 。  把strMachineKey的内容写入紧跟着的buf
 
针对整形和长整形就不说了
大同小异
 
e) 接着我们关注下 是怎么写结构体oActionInfo的。 
 
      1)先写tag。  oActionInfo 的tag为5.  程序里规定tag占两个字节。
     2 ) 接着针对结构体这里 会写4个固定的字节用来存结构体序列化长度。因为开始不知道所以值为0。
      3 ) 接着写字段 DistributorId。  它在oActionInfo结构体中的tag值为6.类型为int64. 所以先写tag=6占两个字节,接着分配4个字节存长度。最后分配8个字节存内容
     4)跟着写DisShopId字段。就不细说了
     5)最后写了2个字节包尾
     6)最后 回写结构体的长度 
 
这里注意下写结构体时候的写法。不注意的话会看错。
1)这里先拿到开始写结构体的buf指针。注意这里是用的int32_t。占四个字节。跟前面保持一直。这里用来的存后面总序列虚化结构体提的总长度。
2)由于刚开始的时候  并不知道后面的结构体会序列化多少个字节。所以这里先写4个字节。
同时把这便宜的4个字节的内存值 设置为0 
bs<<0;  (这里其实建议写成  bs<<int32_t(0) 会好一点。开起来一致)
这里开始没注意。以为写4个字节值为0的 结构体的头。其实这里是放结构体长度的
3)最后第5步。 重新赋值 结构体的长度
1)int32_t* pLen = (int32_t*)bs.getRawBufCur();
2)bs << 0;
3)int32_t iLen=bs.getWrittenLength();
4)Serialize_w(bs);
5)*pLen = bs.getWrittenLength() - iLen;

f)最后对整个_Cao_action_AddActionSupplier_Req写了两个字节的包尾

g) 我么可以看到oActionInfo其实有一堆的字段。但是我们在请求的时候只写了两个字段。所有在序列化的时候也只序列化了两个字段

 

其实我们可以看到我们的这种序列化,很整齐。很规则。比较紧凑。但是并不节省空间。这个里面有很多数据可以压缩的。但是压缩带来一个问题就是解压的时候很消耗cpu的。跟我的业务场景不服和。也没必要。

 

四、序列化解析


其实知道了数据是怎么写入的  解析起来就很容易了。其实这种序列化就是两边约定规则。知道规则以后就可以解析了
        解析的具体步骤就不详细说了。这里说下解析的时候几个特殊的地方

1、因为tag占2个字节。所以函数入参不能大于0xffff. 一个结构体的字段个数不能大于0xffff
 
2、假如前端传入的tag在解析端找不到。解析端会偏移处理下一个tag。所以这是为什么我们可以删除字段的原因。
比如前端传入的结构如下
struct A{
 1:int  aa
 2:int  bb
}
但是服务端后台编译后删除了一个字段
struct A{
    1:int  aa
}
a)如果前端只填了字段aa。  那么解析起来没有任何问题.因为不会把字段bb的任何信息序列化进去。
b)假如前端填了 aa 和 bb字段。
那么服务端在解析的时候。拿到tag2。发现找不到对应的数据。
那么它会偏移4个字节取tag2对应字段内容所占的字节数。比如这里是4.
接着它发现是4.就偏移4个字节。不处理字段值内容。直接取下一个tag进行处理
 
这也就是我们为什么能删除字段的原因。
这样看来我们的函数入参其实也是可以删除的
 
3、我们服务端新增字段重新编译。前端没有对应的tag。根本不会序列化进来。这也是我们可以增加字段的原因。
 
4、解析的时候如果发现tag为0.则会是认为解析结束。所以我们的tag是不能为0的
 
5、这样我们也就能为服务端函数增加入参的。 同一个函数比如前端的入参是4个。服务端可以增加N个. 但是注意不能占用   函数已经用的tag。否则会有问题。而且为了保证函数的统一性。最好别这么做。
 
6、到这里已经很清晰了。 最后再说一次不能改tag对应的类型。
 
 

五、话外

我们的这一套就是远程调用rpc服务。通过我们的序列化。

其实就能了解所谓的RPC服务是什么样的。

说白了,远程调用就是将对象名、函数名、参数等传递给远程服务器,服务器将处理结果返回给客户端。

为了能解析出这些信息。在入参的时候做上标识(这里是打tag).

 

谷歌的protobuf也用过。跟thrift其实差不多但是序列化和反序列的话的具体实现是有些不同的。

谷歌的protobuf更节省空间

 

以前具体看过序列化的源码。觉得序列化反序列化以及rpc很神秘。现在看了源码才发现确实写的确实好,

但是没那么神秘里。其实就是按一定规则组包。所以还是要多看源码啊。

 

我们用的thrift就是 facebook的thrift。但是改了些东西。大体是一样的。

相关文章
WK
|
2月前
|
开发框架 开发工具 C++
C++跨平台框架Qt
Qt是一个功能强大的C++跨平台应用程序开发框架,支持Windows、macOS、Linux、Android和iOS等操作系统。它提供了250多个C++类,涵盖GUI设计、数据库操作、网络编程等功能。Qt的核心特点是跨平台性、丰富的类库、信号与槽机制,以及良好的文档和社区支持。Qt Creator是其官方IDE,提供了一整套开发工具,方便创建、编译、调试和运行应用程序。Qt适用于桌面、嵌入式和移动应用开发。
WK
85 5
|
7月前
|
算法 Linux C++
C++框架设计中实现可扩展性的方法
在软件开发中,可扩展性至关重要,尤其对于C++这样的静态类型语言。本文探讨了在C++框架设计中实现可扩展性的方法:1) 模块化设计降低耦合;2) 使用继承和接口实现功能扩展;3) 通过插件机制动态添加功能;4) 利用模板和泛型提升代码复用;5) 遵循设计原则和最佳实践;6) 应用配置和策略模式以改变运行时行为;7) 使用工厂和抽象工厂模式创建可扩展的对象;8) 实现依赖注入增强灵活性。这些策略有助于构建适应变化、易于维护的C++框架。
546 2
WK
|
2月前
|
C++ 开发者 iOS开发
C++跨平台框架
C++跨平台框架使开发者能够编写一次代码,在多个操作系统和硬件平台上运行,提高开发效率和软件可扩展性。常见的框架包括Qt、wxWidgets、SDL、JUCE等,它们各自具有丰富的功能和特点,适用于不同的应用场景。选择框架时需考虑目标平台、功能需求、学习曲线和社区支持等因素。
WK
80 0
|
5月前
|
Java
JDK序列化原理问题之Hessian框架不支持writeObject/readObject方法如何解决
JDK序列化原理问题之Hessian框架不支持writeObject/readObject方法如何解决
|
5月前
|
数据安全/隐私保护 C++ 开发者
C++框架设计秘籍:解锁可扩展性的神奇密码,让你的代码无所不能!
【8月更文挑战第5天】在C++框架设计中,实现可扩展性至关重要以适应需求变化和新功能的加入。主要策略包括:使用接口与抽象类提供统一访问并允许多种实现;采用依赖注入分离对象创建与依赖管理;运用模板和泛型编程实现代码通用性;设计插件机制允许第三方扩展;以及利用配置文件和动态加载支持灵活的功能启用与模块加载。遵循这些实践能构建出更灵活、可维护及可扩展的框架。
60 1
|
5月前
|
开发框架 缓存 前端开发
基于SqlSugar的开发框架循序渐进介绍(24)-- 使用Serialize.Linq对Lambda表达式进行序列化和反序列化
基于SqlSugar的开发框架循序渐进介绍(24)-- 使用Serialize.Linq对Lambda表达式进行序列化和反序列化
|
5月前
|
XML 存储 JSON
(十二)探索高性能通信与RPC框架基石:Json、ProtoBuf、Hessian序列化详解
如今这个分布式风靡的时代,网络通信技术,是每位技术人员必须掌握的技能,因为无论是哪种分布式技术,都离不开心跳、选举、节点感知、数据同步……等机制,而究其根本,这些技术的本质都是网络间的数据交互。正因如此,想要构建一个高性能的分布式组件/系统,不得不思考一个问题:怎么才能让数据传输的速度更快?
134 1
|
7月前
|
jenkins 测试技术 持续交付
利用C++增强框架的可测试性(Testability)
**C++框架可测试性提升策略**:通过模块化设计、依赖注入、使用Mock对象和Stub、编写清晰接口及文档、断言与异常处理、分离测试代码与生产代码、自动化测试,可以有效增强C++框架的可测试性。这些方法有助于确保代码正确性、健壮性,提高可维护性和可扩展性。示例包括使用类和接口实现模块化,通过构造函数进行依赖注入,以及利用Google Test和Google Mock进行断言和模拟测试。
111 1
|
7月前
|
存储 算法 安全
用C++打造极致高效的框架:技术探索与实践
本文探讨了如何使用C++构建高性能框架。C++凭借其高性能、灵活性和跨平台性成为框架开发的理想选择。关键技术和实践包括:内存管理优化(如智能指针和自定义内存池)、并发编程(利用C++的并发工具)、模板与泛型编程以提高代码复用性,以及性能分析和优化。在实践中,应注意代码简洁性、遵循最佳实践、错误处理和充分测试。随着技术发展,不断提升对框架性能的要求,持续学习是提升C++框架开发能力的关键。
140 1
|
8月前
|
分布式计算 网络协议 大数据
基于C++的分布式计算框架设计与实现
基于C++的分布式计算框架设计与实现
477 2