至今我们还没有涉及到客户部分的操作,现在就讨论一下。客户端通过调用服务器端的GetArray()方法来开始整个处理。客户端将会接收我们在服务器创建的安全对象。客户端的程序则负责将这块字节流变成为一个有效的C++对象。以下摘录了做这部分工作的客户端代码:
// create COM smart pointer from CLSID string
IBlobDataPtr pI( "TestServer.BlobData.1" );
SAFEARRAY *psa ;
// Get the safearray from the server
pI->GetArray( &psa );
file:// create a pointer to an object
CSimpleObj *dummy=NULL;
file:// blob object to eXPand
CBlob blob;
file:// use the blob to expand the safearray into an object
blob.Expand( (CObject*&)dummy, psa );
file:// call a method on the object to test it
dummy->Show(this->m_hWnd, "Client Message");
// delete the object
delete dummy;
这段代码挺简单的。它使用一个COM智能指针与服务器进行连接。假如你对智能指针不熟悉,这些方便的小对象通过调用CoCreateInstance()就可做所有的工作,与服务器进行连接。
CBlob类再一次完成这个步骤,这个类与服务器端的那一个是完全相同的。该类代码事实上是由服务器工程中得到的。将一个安全数组变成一个CSimpleObject的工作由Expand()方法完成。该方法与服务器端调用的Load()方法是相对的。
// Re-create an object from a SAFEARRAY
BOOL CBlob::Expand(CObject * &rpObj, SAFEARRAY *psa)
{
CMemFile memfile; // memory file for de-serialize
long lLength; // number of bytes
char *pBuffer; // buffer pointer
file:// lock Access to array data
SafeArrayAccessData( psa, (void**)&pBuffer );
// get number of elements in array. This is the number of bytes
lLength = psa->rgsabound->cElements;
// attach the buffer to the memory file
memfile.Attach((unsigned char*)pBuffer, lLength);
file:// start at beginning of buffer
memfile.SeekToBegin();
file:// create an archive with the attached memory file
CArchive ar(&memfile, CArchive::loadCArchive::bNoFlushOnDelete);
// document pointer is not used
ar.m_pDocument = NULL;
file:// inflate the object and get the pointer
rpObj = ar.ReadObject(0);
// close the archive
ar.Close();
file:// Note: pBuffer is freed when the SAFEARRAY is destroyed
file:// Detach the buffer and close the file
pBuffer = (char*) memfile.Detach();
file:// release the safearray buffer
SafeArrayUnaccessData( psa );
return TRUE;
}
这个方法的大部分代码你也在前面见过了,该方法接收一个安全数组的输入。我们由安全数组中取出缓冲的数据。CArchive对象负责将缓冲的数据重新创建为对象。
以下就是我们在刚才的Expand()方法中完成的事情
1、锁定访问安全数组结构,并且得到它的长度(以字节计)。
2、将安全数组的数据缓冲与一个CMemFile关联
3、将CMemFile与一个archive相关联
4、使用Archives的ReadObject()方法重新创建对象
5、将SAFEARRAY的数据缓冲由CMemFile脱离
6、返回一个指向新创建对象的指针给调用者
我们在这里谈到了许多基层的东西。CBlob类负责串行化和还原对象的繁重工作。COM部分的程序确实是很易懂的。
性能问题
我并不是劝说你不要使用这里提出的技巧,不过在性能方面,我要提醒你一下。以上我们提到的东西,对于小和中等大小的对象,可工作得很好的。不过,大型的对象将会有问题,我指的是超过一兆的大型对象。
问题是,这些大型对象必须放在内存中,它可令系统开始使用分页。你可能也知道,分页的效率是很低的,非凡是对于内存少的机器。
这些问题产生的原因是几个这样大的对象都必须同时放在内存中。假如客户和服务器应用都运行在同一部机器(或者一个进程中),那么在内存中同时可有高达三个这些对象的拷贝。这样系统开销就会很大,但这又是不可避免的。
对于由许多小对象组成的大对象,在串行化处理方面可以作一些调整。你可以调用自己的CArchive,并且用更大的哈希表调用SetStoreParams和SetLoadParams。CArchive使用一个哈希表来存储类的信息。假如哈希表填满了,那么串行化的处理将会变得很慢。默认的大小适合用在1000或者更少的对象,要了解如何做到这一点,可看一下sample目录的CBigArchive类。你可以使用这个类来代替CArchive。在这个工程的CBigArchive类中,我也包含了它的一个例子(在我的例子中并没有使用CBigArchive)。
安全数组外的选择
安全数组并不是传送二进行数据的唯一方法。我还简要提一下其它三个办法。我可以肯定还有其它的方式。
1、使用一个VARIANT
调用VariantInit API函数可创建一个VARIANT数据。该函数初始化VARIANT的数据结构。你还需要设置variant的类型,这可以通过设置它的"vt"成员做到。以下的代码片段创建一个variant并且将它放到一个安全数组中。
VARIANT *pVar
// initialize the variant
VariantInit( pVar );
file:// set the variant type to an array of bytes
pVar->vt = VT_ARRAY + VT_UI1;
file:// create the safe array pointer
SAFEARRAY *psa;
psa = SafeArrayCreateVector( VT_UI1, 0, llen );
file://Code to load the SAFEARRAY with data
...
file:// assign the SAFEARRAY pointer to the VARIANT
pVar->parray = psa;
2、使用MIDL提供的非自动类型
我们使用SAFEARRAY和VARIANT的原因是可以利用MIDL产生的类库。该类库可以简化调用数据的操作,还提供COM的智能指针。使用一个类库并不是必须的,但是不这样做的话,你需要做一些额外的工作。
MIDL答应一些"custom"(自定义)的数据类型。这些数据类型并不兼容类库和"automation"的接口。一个二进制对象的MIDL接口可以使用一个byte *,而不是一个安全数组。该接口规范需要包含一些关于调用的明确信息,这些信息在[IN]和[OUT]属性中定义。
在TesServer.IDL文件中,我提到了一个关于如何定义MIDL接口的例子。该串行化的处理与这篇文章谈到的是一样的,不过缓冲分配有点不同。以下是IDL的定义。
file:// get data from server
HRESULT GetData([out] long* pcLen,[out,size_is(,*pcLen)] byte **pBuffer );
file:// send data to server
HRESULT SetData([in] long cLen,[in,unique,size_is(cLen)] byte Buffer[]);
要使用这些接口你将必须做一些调用。"size_is"属性告诉MIDL如何调用数据。(IDL属性定义的信息与安全数组的成员变量存储的信息是一样的)。调用通过一个由MIDL产生的Proxy/Stub DLL完成。Proxy/Stub DLL需要建立,并且在客户和服务器端使用。
3、使用IStream接口
一个完全不同的方法是使用标准的IStream接口。我没有研究过,不过一些编程者坚持说这是唯一的方法。我觉得安全数组也不错,因此没有尝试过,这可能会在未来的文章中提到。