设为首页 加入收藏

TOP

从RPC开始(二)、序列化(一)
2017-10-13 10:39:17 】 浏览:811
Tags:RPC 开始 序列化

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

1、序列化容器。

2、序列化方式。

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

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

 

一、容器

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

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

class IBuffer{//没有写所必要的virtual ~IBufer(){}
public:
    virtual void write(const byte* buffer, size_type length) = 0;
    virtual bool read(byte* buffer, size_type length) = 0;
public:
    virtual size_type length()const = 0;
    virtual const byte* data()const{ return nullptr;}
};

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

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

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

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

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

class IBuffer{//依然没有必要的virtual ~IBuffer(){}
public:
    virtual void write(const byte* buffer, size_type length) = 0;
    virtual bool read(byte* buffer, size_type length) = 0;
    virtual void clear() = 0;//刺眼8
    virtual void reset() = 0;//刺眼9
public:
    virtual size_type length()const = 0;
    virtual size_type bufferSize()const = 0;//刺眼1
    virtual const byte* data()const{ return nullptr;}
    virtual const byte* buffer()const{ return nullptr;}//刺眼2
public:
    virtual size_type tellRead()const = 0;
    virtual size_type tellWrite()const = 0;
    virtual size_type seekRead(size_type offset) = 0;
    virtual size_type seekWrite(size_type offset) = 0;
    virtual size_type setLength(size_type length) = 0;//刺眼3
public:
    virtual bool addBuffer(IBuffer* buffer, size_type offset) = 0;//刺眼4
    virtual void reserve(size_type length) = 0;//刺眼5
public:
    virtual void serialize(IBuffer* buffer)const = 0;//刺眼6
    virtual bool deserialize(IBuffer* buffer) = 0;//刺眼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 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇【原创】起步互联网公司内部短信.. 下一篇抽象工厂模式(13)

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目