从RPC开始(二)、序列化

时间:2021-10-19 07:26:12

在C++的世界里构建一个序列化框架;并非一件困难的事情,但也并非简单。因此,需要分成两部分来完成这项任务:

1、序列化容器。

2、序列化方式。

  前者,很容易理解;但也决定着我们将要存储数据的方式:二进制抑或其他。二进制方式,很容易想到和使用的方式;但也最容易以极不安全的方式去使用;因为,为了各种原因,在存储时我们极易丢掉原本的类型信息,使得一切都靠“人工约定”这种很不靠谱的方式。而其他方式,如文本,我们则可以相对地在其中保留很多信息;即使最后的成品并非是让人类来阅读的,但构建过程中,为了各种目的(如调试),总会加入各种信息的。

  后者,则决定了该框架的可用性以及健壮性;在此有两种方式可选择:接口和全局重载函数。第一个,是完全面向对象的方式,也是以侵入式地决定了一个类型是否可以被序列化;看似完美,但对于已完善的不可更改的类型,是有着致命性的不足:无法被我们的框架所包容(例如基本类型)! 第二个,使用类似“operator<<”的cout方案;可以扩展式地支持所有类型,但,是的有“但是”,其在一定程度上破坏了“封装”,C++友元函数便是这一争论的中心。当然,这无关紧要。

一、容器

  第一步,自然我们需要选择是使用固定的一个完善的类来完成这项工作;还是,使用相对可扩展的接口定义。前者,在我们的RPC中是很理想的方式:我们只需要二进制。而后者,则关系到整个序列化框架本身的可复用性——如果,我们想要支撑永久化对象(即:保存到文件和从文件恢复)呢?

  所以,对此,只能使用接口:

class IBuffer{//没有写所必要的virtual ~IBufer(){}
public:
    ;
    ;
public:
    ;
    virtual const byte* data()const{ return nullptr;}
};

  以上便是我们所需要的最基本的接口了。基本上和一个文件所提供的功能一致;需要说明的是“data()”函数,其既不是纯虚的,其也有一个不怎么安全的默认返回值:nullptr。很简单:其是为了兼容MemoryBuffer和FileBuffer而设定的!在MemoryBuffer的实现中我们需要重写该函数以传递二进制序列化后的结果;而FileBuffer则无视这一接口。而且,默认返回一个"nulllptr"也只有一个作用:该函数会返回【空指针】,请注意!

  当然,这并非尽头;在使用该接口时,我们将会十分的难受!因为,没有可设置的偏移量接口;是的,我们需要偏移量,当需要完成更复杂的操作时。比如,我们抛个异常:

    ....
    buffer->read(...);
    throw XXX;

  作为一个健壮的系统,我们需要支持一定程度的【异常安全】:在异常抛出后,我们捕获它,并重置偏移量,然后继续抛出。最经典的场景是:在反序列化的某处,检测到类型不匹配或缓冲区不足,我们需要抛出异常,而非错误地继续走下去。 这是第二部分“序列化方式”的关键之一。

  因此,我们需要如下新的接口:

class IBuffer{//依然没有必要的virtual ~IBuffer(){}
public:
    ;
    ;
    ;//刺眼8
    ;//刺眼9
public:
    ;
    ;//刺眼1
    virtual const byte* data()const{ return nullptr;}
    virtual const byte* buffer()const{ return nullptr;}//刺眼2
public:
    ;
    ;
    ;
    ;
    ;//刺眼3
public:
    ;//刺眼4
    ;//刺眼5
public:
    ;//刺眼6
    ;//刺眼7
};

  上面突然间有了很多刺眼的新接口;因为,我不想浪费篇幅,打算一次性讲完。上面的“tell/seek”系列4个接口,便是新增的偏移量支持,自然不需多说。

  那些“刺眼”系列接口,一定程度上是其作为容器的证明;clear()用来重置整个容器,包括删除所有使用到的内存或硬盘资源;reset()只是简单地重置读写偏移量以复用容器;setLength()/reserve则对应了STL中的resize和reserve;addBuffer()是为了支持直接地从不同的Buffer中写入内容(如MemoryBuffer与FileBuffer的互操作,以不借用第三方资源的方式)。

  bufferSize()与buffer(),需要特殊解释一下;在TCP发送时,我们需要一个头来存放整个消息的大小,否则我们需要发送2次(第一次为消息大小,第二次为消息本身)。所以,为了支持能够获得一个自身带有大小的缓冲区,我们需要这样的接口支持:buffer()返回包含内容大小为头的缓冲区;bufferSize()获得含头的缓冲区大小。

  在以上接口的支持下,我们便能够完成所需要的所有事情了;当然,你需要实现它们。

二、方式

  面向对象语言(编译型),都有一个问题:值类型和引用类型的隔阂。同样在C++中也有类似的问题:基本类型和自定义类。前者,我们无法做任何的改变;但他们和后者有着巨大的距离:没有成员函数。意味着,我们只能够以函数重载的方式去兼容二者:

void Serialize(IBuffer* buffer, int);//基本类型1

void Serialize(IBuffer* buffer, double);//基本类型2

void Serialize(IBuffer* buffer, const Something&);//自定义类型1

  作为一个现代的C++人士,我们不能够容忍这样的“无聊”;因为我们可以进行类型萃取:

  1、如果为基本类型,我们直接保存其二进制值;

  2、如果其为自定义类,且有成员函数“serialize/deserialize”,我们通过调用该配套函数进行序列化和反序列化;

  3、如果其为自定义类,且是POD,同样直接保存其二进制值;

  4、以上的其他情况,编译器会直接报错。

  以上方案,可以通过C++11完成。至于方式,其涉及模板元编程;我并不详细描述,大致通过以下方式完成:

  1、使用TypeList定义基本类型的类型列表(当然可以逐个特化,但这样失去了味道),以判断是否为基本类型;

  2、使用C++11的decltype判断是否有“serialize/deserialize”成员函数;

  3、在类型信息中查找该类型是否为POD且不为指针。

  然后,使用元编程将以上3个信息融合得到最佳的选择。详细的,可以参考未来的某天,我将要讲的《基于C++11的类型系统》。或者,可以依次搜索:泛型编程与设计模式之应用(TypeList)、C++SFINAE(关键技术decltype)、STL的type_traits。

  有一个关键的地方不得不提:不为指针。是的,我没有支持指针的序列化;因为,我们并不能够知道一个指针是一个元素还是一个数组。当然,我们可以支持shared_ptr和vector,以更健壮的方式。

  总结一下,在你学完所有以上提到的及术后,我们便能够创建出以下序列化方式:

  1、基本类型和自定义POD调用以下方式:

template<typename T>
void _Serialize(IBuffer* buffer, const T& val)
{
    buffer->write((const byte*)&val, sizeof(T));
}

template<typename T>
bool _Deserialize(IBuffer* buffer, T& val)
{
    return buffer->read((byte*)&val, sizeof(T));
}

  2、自定义类型且有配套函数:

template<typename T>
void _Serialize(IBuffer* buffer, const T& val)
{
    val.serialize(buffer);
}

template<typename T>
bool _Deserialize(IBuffer* buffer, T& val)
{
    return val.deserialize(buffer);
}

  还有一个问题就是,对于已完善的不可更改类的处理:很简单和自然,重载以上_Serialize/_Deserialize函数;比如STL中的std::string就需要如此处理。

  还有一个不可忽视的问题:健壮性!也就是,我们需要感知类型,我们需要类型信息。很自然的方式,我们在序列化时写入类型信息;在反序列化首先获取该信息,并判断是否类型匹配。

  可用的方式是:写入类型ID;通常来说是一个字符串。但,问题的关键是谁来生成并提供这个字符串?

  自然,依然使用C++的方式:模板特化

template<>
struct TypeInfo<Something>{
    static std::string Name(){ return "Something";}
};

  这种方式,很繁琐,意味着:对于每一个需要序列化的类型,我们都需要特化一个模板!但,这是必不可少的,因为我们没有完整的运行时类型系统,我们可以操作类型的唯一机会,只在编译期。不要抱怨,每个类,也只需要一次而已,代价并不高。

  完了。?没有,这个“判断过程”由谁来做?绝不是手动进行。到这里,我们需要引入一个“代理”:Any,可变类型。也就是,其可以包装所有类型;然后通过模板函数来获取值:

template<typename T>
void Any::from(const T& val);

template<typename T>
T& Any::to();

template<typename T>
const T& Any::to()const;

  在序列化时,我们需要首先将类型包装到Any中,然后通过Any执行序列化;在另一端,我们将Buffer交给Any然后再调用to<T>()时反序列化;这时,我们就有一个中间层帮我们完成类型信息的写入和匹配:

void _AnyData::serialize(IBuffer* buffer)
{
    Serialize(buffer, TypeInfo<T>::Name());
    Serialize(buffer, mData);
}

bool _AnyData::deserialize(IBuffer* buffer)
{
    std::string name;
    if(!Deserialize(buffer, name)){
        return false;
    }
  if(name != TypeInfo<T>::Name()){
    return false;
  }
Deserialize(buffer, mData);
  return true; }

  当然,以上仅仅是示例并非可用的代码;但,也差不多了。这样,我们便能够完成序列化和反序列化的完整操作,并且在安全的环境下。当然需要说明一下所谓的安全:是指我们能够感知错误的反序列化,并有机会进行相应的处理。Deserialize返回布尔值,便是作为一个通知(我并没有选择抛出异常)。

  以上,便是序列化框架的所有了;当然,并没有列出所有的内容;但也,足够说明,C++能够以我的方式构造出一个相对安全且可用的序列化。

  PS:很多东西,我并没有展开;有机会的话,可以等开源我的代码;但很渺茫。

从RPC开始(二)、序列化的更多相关文章

  1. JAVA RPC(二)序列化协议杂谈

    序列化和反序列化作为Java里一个较为基础的知识点,大家心里也有那么几句要说的,但我相信很多小伙伴掌握的也就是那么几句而已,如果再深究问一下Java如何实现序列化和反序列化的,就可能不知所措了!遥记当 ...

  2. 聊聊RPC原理二

    之前写了一篇关于RPC的文章,浏览量十分感人:),但是感觉文章写得有些粗,觉得很多细节没有讲出来,这次把里边的细节再次补充和说明. 这次主要说的内容分为: 1. RPC的主要结构图. 2.分析结构图的 ...

  3. 老王讲自制RPC框架&period;&lpar;四&period;序列化与反序列化)

    #(序列化) 在实际的框架中,真正影响效率的就是数据的传输方式,以及传输的准备,或者说是tcp与http,序列化.当然要想提高整个框架的效率,需要采用一种高效的序列化 框架比如流行的protostuf ...

  4. android ipc通信机制之二序列化接口和Binder

    IPC的一些基本概念,Serializable接口,Parcelable接口,以及Binder.此核心为最后的IBookManager.java类!!! Serializable接口,Parcelab ...

  5. Thrift RPC实战&lpar;二&rpar; Thrift 网络服务模型

    限于篇幅关系,在观察源码的时候,只列举了部分源代码 TServer类层次体系 TSimpleServer/TThreadPoolServer是阻塞服务模型 TNonblockingServer/THs ...

  6. django rest&lowbar;framework入门二-序列化

    在前一节中,我们已经粗略地介绍了rest_framework的作用之一序列化,下面我们将详细探究序列化的使用. 1.新建一个app snippets python manage.py startapp ...

  7. RPC里面的序列化反序列化以及拆包粘包

    1.序列化(1)什么是序列化? Java的序列化是把对象转换成有序字节流的过程.以便进行网络传输或者保存到本地.(2)为什么要序列化? 当两个进程进行远程通信时,如果需要发送各种各样的数据,文本.音频 ...

  8. Python模块&lpar;二&rpar;&lpar;序列化&rpar;

    1. namedtuple 命名元组->类似创建了一个类 from collections import namedtuple p = namedtuple("Point", ...

  9. 老王讲自制RPC框架&period;&lpar;二&period;动态代理)

    (#简介) 什么是动态代理?动态代理是实现阶段不关心代理是谁,而在运行阶段才指定代理对象是哪一个,动态代理在做框架方面使用非常 广泛,比如spring的aop,其核心就是采用动态代理机制,下面让我们来 ...

  10. Netty实现高性能RPC服务器优化篇之消息序列化

    在本人写的前一篇文章中,谈及有关如何利用Netty开发实现,高性能RPC服务器的一些设计思路.设计原理,以及具体的实现方案(具体参见:谈谈如何使用Netty开发实现高性能的RPC服务器).在文章的最后 ...

随机推荐

  1. Java连接SQLServer2008终极解决办法(亲身上机演练版)

    今天我一学妹问我,Java连接SQLServer2008数据库的问题,一直无法连接成功.想起自己刚开始学习的时候,在网上找各种文章,然后实际上机验证操作,花了一两天时间才搞定,一把辛酸泪呀!记得当时是 ...

  2. 第四节:Vue表单标签和组件的基本用法,父子组件间的通信

    vue表单标签和组件的基本用法,父子组件间的通信,直接看例子吧. <!DOCTYPE html> <html> <head> <meta charset=&q ...

  3. Quartz结合SPRING多任务定时调用

    定义两个被调度的类 public class QuartzJob { public void work() { System.out.println(Spring Quartz的任务调度1被调用!&q ...

  4. c&plus;&plus;中的&amp&semi;

    变量的前面表示取变量地址赋值给指针, 如:int a = 0; int *pa = &a;类型后面表示引用,引用即变量的替身. int a = 0; int &ref = a;操作re ...

  5. 3-4 rpm包查询

    概述:yum不能查询已经安装好的rpm包, 就算采用了yum来进行安装,查询方法还是依赖rpm包的查询, 因此rpm包的查询十分常用和重要 1.查询是否安装 <1>rpm -q 包名(不是 ...

  6. ASP&period;Net数据库如何存取图片

    当我们有大量的图片或者图片比较大时,我们常规的做法可能是保存图片路径,但是也不排除需要将图片直接存放到数据库的情况,此时就需要保存图片到数据库了.这篇文章我会向大家介绍: 如何通过FileUpLoad ...

  7. 剑指offer第三天

    21.栈的压入.弹出序列 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序.假设压入栈的所有数字均不相等.例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3, ...

  8. SpringCloud学习之Ribbon

    一.负载均衡与Ribbon 负载均衡,在集群中是很常见的一个“名词”,顾名思义是根据一定的算法将请求分摊至对应的服务节点上,常见的算法有如下几种: 轮询法:所有请求被依次分发到每台应用服务器上,每台服 ...

  9. python爬虫实例项目大全

    WechatSogou [1]- 微信公众号爬虫.基于搜狗微信搜索的微信公众号爬虫接口,可以扩展成基于搜狗搜索的爬虫,返回结果是列表,每一项均是公众号具体信息字典. DouBanSpider [2]- ...

  10. 一天一个Linux命令--dhclient

    dhclient -r #用于释放ip地址2 dhclient #获取IP地址 主要针对只有命令行的Linux机器,临时改变了网络环境,事先手动设置的ip地址 看一下自带的解释 root@ubuntu ...