三沣开发知识 购物 网址 游戏 小说 歌词 地图 快照 开发 股票 美女 新闻 笑话 | 汉字 软件 日历 阅读 下载 图书馆 编程 租车 短信 China
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
多播视频美女直播
↓电视,电影,美女直播,迅雷资源↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
vbs/VBScript DOS/BAT hta htc python perl 游戏相关 VBA 远程脚本 ColdFusion ruby专题
autoit seraphzone PowerShell linux shell Lua Golang Erlang 其它教程 CSS/HTML/Xhtml
html5 CSS XML/XSLT Dreamweaver教程 经验交流 开发者乐园 Android开发资料
站长资讯 .NET新手 ASP.NET C# WinForm Silverlight WCF CLR WPF XNA VisualStudio ASP.NET-MVC .NET控件开发 EntityFramework WinRT-Metro Java C++ PHP Delphi Python Ruby C语言 Erlang Go Swift Scala R语言 Verilog 其它语言 架构设计 面向对象 设计模式 领域驱动 Html-Css JavaScript jQuery HTML5 SharePoint GIS技术 SAP OracleERP DynamicsCRM K2 BPM 信息安全 企业信息 Android开发 iOS开发 WindowsPhone WindowsMobile 其他手机 敏捷开发 项目管理 软件工程 SQLServer Oracle MySQL NoSQL 其它数据库 Windows7 WindowsServer Linux
  IT知识库 -> 架构设计 -> 从RPC开始(二)、序列化 -> 正文阅读
 

[架构设计]从RPC开始(二)、序列化

从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、如果其为自定义类,且有成员函数“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;
  }
return Deserialize(buffer, mData); }

  当然,以上仅仅是示例并非可用的代码;但,也差不多了。这样,我们便能够完成序列化和反序列化的完整操作,并且在安全的环境下。当然需要说明一下所谓的安全:是指我们能够感知错误的反序列化,并有机会进行相应的处理。Deserialize返回布尔值,便是作为一个通知(我并没有选择抛出异常)。
  以上,便是序列化框架的所有了;当然,并没有列出所有的内容;但也,足够说明,C++能够以我的方式构造出一个相对安全且可用的序列化。
  PS:很多东西,我并没有展开;有机会的话,可以等开源我的代码;但很渺茫。
  架构设计 最新文章
spring boot实现ssm(2)功能
java 企业站源码 兼容手机平板PC 自适应响应
Serverless无服务应用架构纵横谈
理论篇:关注点分离(Separation of concern
Struts 2 入门
spring boot实现ssm(1)功能
atitit.日期,星期,时候的显示方法ISO 8601标
聊聊数据库级联删除与伪删除的设计方案
APP多版本共存,服务端如何兼容?
实时监控Cat之旅~介绍与自定义类型在哪里
上一篇文章      下一篇文章      查看所有文章
加:2017-01-24 01:54:02  更:2017-05-16 02:59:54 
 
技术频道: 站长资讯 .NET新手区 ASP.NET C# WinForm Silverlight WCF CLR WPF XNA Visual Studio ASP.NET MVC .NET控件开发 Entity Framework WinRT/Metro Java C++ PHP Delphi Python Ruby C语言 Erlang Go Swift Scala R语言 Verilog 其它语言 架构设计 面向对象 设计模式 领域驱动设计 Html/Css JavaScript jQuery HTML5 SharePoint GIS技术 SAP Oracle ERP Dynamics CRM K2 BPM 信息安全 企业信息化其他 Android开发 iOS开发 Windows Phone Windows Mobile 其他手机开发 敏捷开发 项目与团队管理 软件工程其他 SQL Server Oracle MySQL NoSQL 其它数据库 Windows 7 Windows Server Linux
脚本语言: vbs/VBScript DOS/BAT hta htc python perl 游戏相关 VBA 远程脚本 ColdFusion ruby专题 autoit seraphzone PowerShell linux shell Lua Golang Erlang 其它教程
网站开发: CSS/HTML/Xhtml html5 CSS XML/XSLT Dreamweaver教程 经验交流 开发者乐园 Android开发资料
360图书馆 软件开发资料 文字转语音 购物精选 软件下载 美食菜谱 新闻资讯 电影视频 小游戏 Chinese Culture 股票 租车
生肖星座 三丰软件 视频 开发 短信 中国文化 网文精选 搜图网 美图 阅读网 多播 租车 短信 看图 日历 万年历 2018年2日历
2018-2-19 16:14:51
多播视频美女直播
↓电视,电影,美女直播,迅雷资源↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT知识库