至今我們還沒有涉及到客戶部分的操作,現在就討論一下。客戶端通過調用服務器端的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接口。我沒有研究過,不過一些編程者堅持說這是唯一的方法。我覺得安全數組也不錯,因此沒有嘗試過,這可能會在未來的文章中提到。