实战经验:采用MFC的CArchive将对象序列化到CMemFile中
在一次编程实验中,我碰到了这样一个问题:我需要将一个对象通过网络传输出去。
首先为了可扩展性,这个对象被设计为变长结构,也即我们事先不能知道它确切的长度。有些朋友会说,可以创建一个足够大的缓冲区来承载这个对象。嗯!当然是可以的。但是这样的设计不是最优的,因为网络带宽是有限的,从节省带宽的角度来说,使用大缓冲区肯定意味着有些不必要的数据和有效数据一起传输。另外,对象长度变长,缓冲区的最大尺寸,还真是不太好确定。那么,有什么好的办法呢?对象序列化!
MFC序列化支持
在MFC的体系结构中,框架提供了CArchive这样一个基础设施帮助我们完成对象的序列化。对于这个对象来说,需要满足以下约束条件:
1) 此对象从CObject类继承。
2) 此对象声明并实现了Object Serialization。这个很简单,我们直接在对象的声明和实现文件中,添加对应的宏就可以了。
3) CArchive类仅支持内建简单类型的序列化,例如int, char,CString等。复杂的数据类型,例如vector,不被支持。
首先我们看一个支持序列化的对象代码
类声明
#pragma once class CPerson : public CObject { DECLARE_SERIAL(CPerson) public: CPerson(); virtual ~CPerson(); public: virtual void Serialize(CArchive& ar); public: CString name; int age; char sex; };
类实现
#include "stdafx.h" #include "Person.h" IMPLEMENT_SERIAL(CPerson, CObject, VERSIONABLE_SCHEMA | 1); CPerson::CPerson() { } CPerson::~CPerson() { } void CPerson::Serialize(CArchive& ar) { __super::Serialize(ar); if (ar.IsStoring()) { ar << age << sex << name; } else { ar >> age >> sex >> name; } }
为了方便测试,这里我将对象的数据成员设置成了public。
从以上代码,我们可以看到
1) CPerson继承自CObject,这样就可以使用CObject内建的序列化基础设施。
2) 声明中使用了DECLARE_SERIAL宏,实现中使用了IMPLEMENT_SERIAL,使用宏的方式,将序列化相关的支持代码插入到源码中,提高了代码的统一性和抽象化。这里要注意的是IMPLEMENT_SERIAL的第三个参数,我使用了VERSIONABLE_SCHEMA | 1,这个参数主要用于序列化的版本兼容性。简单来讲,当我们将一个1.0版本的对象序列化到文件中,我们可以使用1.0版本的代码从文件中反序列化来重建对象。当代码版本升级到2.0,通过版本标识,新版本的代码可以在反序列化过程中首先判断文件中保存的是1.0版本的对象,还是2.0版本的对象,从而能对不同版本的文件进行分别处理,这样,从用户角度来看,软件的版本升级了,但它依然可以读取旧版本软件创建的文件格式。
3) 我们重写了CObject的Serialize,将对象自身的数据成员序列化到CArchive中。
当我们需要将对象经由网络发出,我们倾向于将对象先序列化到内存中,而不是序列化到文件。因为文件的IO相比内存IO,需要更多的时间,另外,还需要考虑临时文件的删除及并发访问时的冲突问题。
使用CMemFile来承载序列化后的对象
我们直接先给出代码
CPerson p; p.age = 20; p.sex = _T('F'); p.name = _T("Test"); // 序列化 CMemFile memFile; CArchive arStore(&memFile, CArchive::store); arStore.WriteObject(&p); arStore.Flush(); arStore.Close(); int length = memFile.GetLength(); BYTE * pBuf = memFile.Detach(); // TODO: 使用pBuf进行网络传输 // 当不再使用pBuf,需要进行手动删除 free(pBuf);
代码解析
1) 首先创建对象并初始化其数据成员。
2) 定义一个CMemFile对象,并将它作为参数传给CArchive的构造函数。因为CMemFile继承自CFile,所以CArhive在构造时可以接受一个CMemFile对象。
3) 调用CArchive的WriteObject方法,将对象序列化到CMemFile,也即将对象序列化到一块内存块中。
4) 调用CArchive的Flush和Close方法,完成序列化过程。
5) 此时,我们可以调用CMemFile的公开方法得到此对象内存块的信息。例如其长度及内存块指针。
6) 当我们不再使用这块内存块时,需要手动释放内存块,否则会导致内存泄漏。
对象的反序列化
当网络对端接收到经过序列化的对象后,需要有能力将这个字节流进行反序列化,最终重建对象。
以下是反序列化的代码
// 从网络中接收对象字节流 int nLength = <Receive object length from network>; BYTE * pBuf = <Receive object data from network>; // 反序列化 CMemFile memRestore; memRestore.Attach(pBuf, nLength); CArchive arLoad(&memRestore, CArchive::load); CPerson * pRestore = dynamic_cast<CPerson *>(arLoad.ReadObject(RUNTIME_CLASS(CPerson))); arLoad.Close(); // 使用反序列化后的对象 // 当不再使用对象时,需要手动删除 delete pRestore;
代码解析
1) 在网络对端,我们需要接收两个信息用于对象重建:对象字节流的长度和对象字节流数据本身。
2) 得到这两个信息后,通过CMemFile的Attach方法,将内存块挂载到CMemFile。
3) 创建CArchive,并将CMemFile对象进行绑定。使用了CArchive::load表明我们是要从内存中反序列化对象。
4) 调用CArchive的ReadObject方法完成对象重建。
5) 对象使用完毕,记得手动删除。
结论
1) MFC的对象序列化虽然是十几年前设计的,但在今天依然能良好的工作。市场上当然有其他的或大或小的对象序列化框架,这个就需要根据项目的实际情况进行选择了。
2) 基于CArchive的序列化的性能,还有待进一步测试。
相关推荐
- 实战经验:CRichEditCtrl插入图片(不止是位图)
- Posted on 04月21日
- 警惕 C++ 中的隐式类型转换
- Posted on 08月13日
- 关于Tweak UI 2.10的更多问题与解答
- Posted on 11月03日
- 官宣另一个官宣:MySQL在VS2019上的构建和调试
- Posted on 09月01日
评论已关闭。