实战经验:采用MFC的CArchive将对象序列化到CMemFile中

实战经验:采用MFC的CArchive将对象序列化到CMemFile中

作者:BlogUpdater |  时间:2019-02-24 |  浏览:6042 |  评论已关闭 条评论

在一次编程实验中,我碰到了这样一个问题:我需要将一个对象通过网络传输出去。
首先为了可扩展性,这个对象被设计为变长结构,也即我们事先不能知道它确切的长度。有些朋友会说,可以创建一个足够大的缓冲区来承载这个对象。嗯!当然是可以的。但是这样的设计不是最优的,因为网络带宽是有限的,从节省带宽的角度来说,使用大缓冲区肯定意味着有些不必要的数据和有效数据一起传输。另外,对象长度变长,缓冲区的最大尺寸,还真是不太好确定。那么,有什么好的办法呢?对象序列化!

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的序列化的性能,还有待进一步测试。

标签:

评论已关闭。