日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > windows >内容正文

windows

封送处理您的数据:利用 COM 和 Windows 2000 的高效传输数据的技术

發布時間:2023/12/9 windows 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 封送处理您的数据:利用 COM 和 Windows 2000 的高效传输数据的技术 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

From:?http://blog.csdn.net/zhoujianhei/article/details/1844319

摘要 您所選擇的數據傳輸方式在分布式應用程序中是非常重要的。Windows 2000 提供了幾種新的特性,可以更加高效地進行數據傳輸。輕量級的處理程序使得您能夠編寫智能代理,它們能夠緩存結果并且執行帶緩沖的讀寫操作,從而將網絡調用的次數減至最小。Windows 2000 也使得您能夠使用管道接口通過一種預讀的設施來高效地傳輸大量的數據。

本文闡述了利用這些新特性在 Windows 2000 中提高數據傳輸效率的幾種方法。它同時也報告了傳輸時間測試的結果,還給出了傳輸緩沖區大小的推薦標準。

本頁內容
封送處理數據
指定使用 IDL 傳輸的數據
使用類型庫封送處理進行數據傳輸
使用流對象傳輸數據
提高數據傳輸性能
緩沖區大小和跨機器的調用
封送處理對象
通過傳值的方式進行封送處理
處理程序封送處理
使用管道傳輸數據
異步管道
總結

不論分布式應用程序的目的如何,幾乎必然要有的一項需求是:高效的數據傳輸。在本文中,我將討論使用 COM 和 Windows 2000 在網絡上傳輸大量數據的方法,以及在此過程中封送處理所起的作用。我也會討論關于數據緩沖區大小的問題,并且會解釋使您能夠優化傳輸緩沖區大小的一些策略。我將集中討論 COM,因為 COM 是一個基礎結構,它將許多基于 Windows 的組件存放在一起。此外,我會描述 Windows 2000 中提供的大量數據傳輸設施。

封送處理數據

在討論 Windows 2000 幫助您傳輸數據的方法之前,我會先說一說數據是如何從一個機器移動到另一臺機器上的。COM 起源于 Microsoft 遠程過程調用 (RPC)。實際上 DCOM 本質上是“對象 RPC”,而且 DCOM 經常被稱為 ORPC(我認為這是比 DCOM 更準確的術語)。鑒于其產生的根源,COM 繼承了 RPC IDL 作為其描述接口的方法。IDL 通過使用 Microsoft IDL 編譯器 (MIDL) 進行編譯,它要做三件事情。首先,它生成一個接口的描述(例如,類型庫)。第二,它生成語言綁定從而允許在您選擇的語言中使用接口(只有 C 和 C++,所有其他的語言必須要使用類型庫)。最后,它生成 C 代碼,這個代碼能夠編譯產生代理-存根 DLL 來封送接口。

這些代理-存根 DLL 偵聽對接口的調用,并且包含使方法調用能夠跨越上下文邊界的代碼。體系結構如圖 1 所示。


圖?1?COM 封送處理體系結構

從概念上講,客戶端代碼能夠直接訪問組件。但是在下面的兩個對象 £ 接口代理和存根是在客戶端和組件的上下文中由 COM 自動加載的。如果上下文在不同的進程或者不同的機器上,那么 COM 會提供一個通道對象利用 RPC 來傳輸代理和存根初始化緩沖區。該通道對象基于 RPC 實現,但是概念上它在導入的和導出的上下文都是可訪問的。代理將方法參數封裝到一個從通道獲得的緩沖區中,而存根獲得該緩沖區并且使用它構造堆棧幀來調用組件。

當組件原來的接口指針首次將組件的上下文封送處理到客戶端的上下文中時,COM 會加載接口代理和存根。在標準的封送處理中,COM 將接口指針作為一個參數傳遞給 CoMarshalInterface。該函數接收特定于上下文的接口指針,然后將其轉換成上下文中立的字節塊,該字節塊描述了組件和正在被封送處理接口的精確位置。數據塊在客戶端的上下文中進行取消封送的處理,將此上下文中立的數據塊轉換成特定于上下文的聚合到代理管理器中的接口代理對象(代理管理器也提供代理對象的標識)。

關于該體系結構重要的是代理對象看起來與原始對象一模一樣。接口存根熟知接口的信息,并且具有類似一個上下文中的客戶端的行為。組件和它的客戶端不知道封送處理,也不知道封送處理是如何實現的。這是因為代理和存根對象偵聽了對組件的調用。COM 封送處理僅僅偵聽方法調用,并且將它們在上下文之間傳輸。但可以看到,其他的偵聽代碼可能包含用于優化網絡調用的代碼。我會解釋如何編寫這樣的偵聽代碼,以及 Microsoft 為此已經提供的一些代碼。

返回頁首

指定使用 IDL 傳輸的數據

生成接口代理和存根對象的最簡單方法就是用 IDL 描述接口,使用 MIDL 來生成代碼。關于 IDL 數組的介紹,我推薦 1998 年 8 月的 MSJ 中的“Understanding Interface Definition Language: A Developer's Survival Guide”一文。更完整的描述,請閱讀 1996 年 11 月 MSJ 中的?ActiveX?/COM Q&A。

IDL 用于描述在一次調用中傳輸的數據量以及傳輸的方向(從客戶端到組件或者反之)。方向通過 [in] 和 [out] 屬性來指明,數據量(數組的最大大小)通過 [size_is()] 或者等價的 [max_is()] 來指明。實際的數據項的數目通過 [length_is()] 指明。[size_is()] 屬性向代理指明有多少數據會傳輸到存根處,代理利用這個信息來決定應該從通道中請求多大的緩沖區以及會有多少字節數據復制到這個緩沖區中。有時被傳輸的數組可能不會完整地被填充,因此用 [length_is()](或者等價的 [last_is()])作為一種優化措施來減少從客戶端向組件傳輸不必要的字節數。

下面是如何利用這些屬性的一些示例:

HRESULT PassLongs([in] ULONG ulNum, [in, size_is(ulNum)] LONG* pArrIn); HRESULT GetLongs([in] ULONG ulNum, [out, size_is(ulNum)] LONG* pArrOut); HRESULT GetLongsAlloc([out] ULONG* pNum, [out, size_is(, *pNum)] LONG** ppArr);

當從客戶端向組件傳輸數據時,客戶端總是分配存儲區,并且負責釋放該存儲區。在前面的示例中,在客戶端的代碼中 uINum 參數最有可能是一個自動的變量,pArrIn 是在一個至少有 uINum 個 LONG 的數組中的第一個元素的指針,它可能分配在堆中或者堆棧中。既然使用了 [size_is()] 屬性,這就表明封送拆收器僅僅傳輸 uINum 個數據項。

當從組件向客戶端傳輸數據時,客戶端向存儲區中傳遞一個指針,在存儲區中數據會被封送拆收器復制。因此可以如下這樣調用 GetLongs:

ULONG ulNum = 10; LONG l[10]; hr = pArr->GetLongs(ulNum, l);

組件代碼類似下面所示:

STDMETHODIMP CArrays::GetLongs(ULONG ulNum, LONG *pArr) { for (ULONG x = 0; x < ulNum; x++) pArr[x] = x; return S_OK; }

正如您所看到的,組件代碼假定通過 pArr 指針可以訪問數據存儲區。組件一端的封送拆收器會分配足夠的存儲空間,因為 [size_is()] 屬性已經告知了所需要的空間大小。

正如我前面提到的,客戶端負責釋放存儲空間。在這種情況下,因為已經在堆棧中使用了自動變量因此不需要額外的代碼。這種技術假定客戶端知道有多少個數據項是可用的。

如果客戶端在從組件請求數據之前不能確定數據項的數目,應該怎樣處理呢?看一下前面所示的那個包括了 GetLongsAlloc 的示例。這里,組件通過 pNum 參數返回了返回的數組的大小。然而,因為大小是由組件方法來判定的,因此封送拆收器在方法調用之前將不能有足夠信息來分配存儲空間。所以組件必須分配內存。組件通過使用封送處理層所知道的內存分配器 CoTaskMemAlloc 來完成這個工作。

STDMETHODIMP CArrays::GetLongsAlloc(ULONG *pNum, LONG **ppArr) { *pNum = 10; *ppArr = reinterpret_cast(CoTaskMemAlloc (*pNum * sizeof(LONG))); for (ULONG x = 0; x < *pNum; x++) (*ppArr)[x] = x; return S_OK; }

內存不是由組件釋放的,如果組件和客戶端位于不同的機器上,起初這看起來有點像是內存泄漏。但事實并非如此。當組件端的封送處理代碼將數據傳輸到 RPC 時,它將調用 CoTaskMemFree 來釋放組件端的緩沖區。在客戶端,封送拆收器會知道 *pNum 個項已經發送出去,將會再次調用 CoTaskMemAlloc 來在客戶端中復制這個數組,然后將數據項復制到其中。客戶端然后就能夠訪問這些數據項了,但是必須調用 CoTaskMemFree 來釋放數組占用的內存:

ULONG ulNum; LONG* pl; hr = pArr->GetLongsAlloc(&ulNum, &pl); for (ULONG ul = 0; ul < ulNum; ul++) printf("%ld/n", pl[ul]); CoTaskMemFree(pl);

數據項的數目和數組的指針從 GetLongAlloc 返回到客戶端。因此 pl 的地址傳遞到方法,并且 IDL 有奇怪的符號標記

[out, size_is(, *pNum)] LONG** ppArr

[size_is()] 中的逗號表示 *pNum 是 ppArr 所指的數組的大小。

如果您使用我提到的任何一個數組屬性,都必須通過編譯和鏈接 MIDL 產生的 C 文件而生成一個代理-存根 DLL。為了做到這一點,ATL AppWizard 生成一個 make 文件,名稱是?projectps.mk。必須確保服務器沒有將組件的接口作為自動封送拆收器封送處理的類型庫而注冊,因為自動封送拆收器不會識別數組屬性。

返回頁首

使用類型庫封送處理進行數據傳輸

如果客戶端使用類型庫封送處理怎么辦?您有兩個選擇。您可以使用一個 BSTR 或者一個 SAFEARRAY 來傳輸數據。一個 BSTR 是預先確定長度的 OLECHAR(每個 16 位)緩沖區,但是您可以通過調用 SysAllocStringByteLen 來讓 COM 創建一個 8 位字節的數組:

// pass NULL for the first parameter to // get an uninitialized buffer BSTR bstr = SysAllocStringByteLen(NULL, 10); LPBYTE pv = reinterpret_cast(bstr); for (UINT i = 0; i < 10; i++) pv[i] = i * i;

MIDL 將基于 BSTR 是長度預先確定的這一事實來為其生成封送處理代碼。為了看到這種行為,向一個接口方法中增加一個 BSTR,同時查看 MIDL 所產生的這一封送處理文件?project_p.c。就會發現 BSTR 是使用 BSTR_UserSize、BSTR_UserMarshal、BSTR_UserUnmarshal 和 BSTR_UserFree(由 OLE32.dll 提供)這些函數由用戶進行封送處理的。

這些封送拆收器例程使用 BSTR 前綴來判斷要傳輸多少個字節。它們并不把數據解釋為字符串,因此數據可能是具有嵌入空值的二進制數據。如果數據位于一個 BSTR 中,當用 Visual Basic 編寫應用程序時自然會利用這一點。盡管這是可能的,但是 Visual Basic 對于 BSTR 已經幫助您做了很多工作,并且您必須撤銷一些工作來訪問它的數據。

例如,如果您有下面這個方法:

HRESULT GetDataInBSTR([out, retval] BSTR* pBstr);

那么就能使用 Visual Basic 訪問 BSTR 中的二進制數據:

Dim obj As New DataTransferObject Dim s As String Dim a() As Byte ' get the BSTR s = obj.GetDataInBSTR() ' convert it to a Byte array a = s ' now do something with the data For x = LBound(a) To UBound(a) Debug.Print a(x) Next

在 C++ 中用 ATL 完成同樣的事情大致需要用同樣多的代碼,然而一般對于 COM 代碼來說情況并非如此。

CComPtr pObj; pObj.CoCreateInstance(__uuidof(DataTransfer)); CComBSTR bstr; // get the BSTR pObj-> GetDataInBSTR(&bstr); // get the number of bytes in the BSTR UINT ui = SysStringByteLen(bstr.m_str); LPBYTE pv = reinterpret_cast(bstr.m_str); // do something with them for (UINT idx = 0; idx < ui; idx++) printf("array[%d]=%d/n", idx, pv[idx]);

將二進制數據放進 BSTR 的另一個問題是大多數的包裝類假定數據是 Unicode 字符串。我顯式地調用 SysStringByteLen 來獲得 BSTR 中字節的數量,因為 CComBSTR::Length 將返回 BSTR 中的 Unicode 字符的數量。

傳遞數據的另一個方法是通過 Visual Basic 的 SAFEARRAY(關于 SAFEARRAY 的詳細信息,請參見 1996 年 6 月一期的?MSJ?專欄OLE Q&A)。SAFEARRAY 是自描述的,它們包含一個數組中項類型的描述和維數的描述以及每一維度大小的描述。這些信息組合起來使得封送拆收器確切知道應該傳輸多少個字節。這個技術帶來的另外一個好處是如果 SAFEARRAY 包含了 VARIANT,那么數據對于腳本客戶端就是可讀的。但是,必須證明對于每一個 VARIANT 項的 16 字節開銷能夠容納一個單個的數據字節。

返回頁首

使用流對象傳輸數據

我想要提及的傳輸數據的最后一個方法是使用流對象。IStream 指針可以由類型庫封送處理進行封送,并且可以被 C++ 客戶端訪問。然而,它們不是通過 Visual Basic 代碼直接訪問的。(在 Visual Basic 中的持久對象確實支持 IPersistStream 和 IPersistStreamInit,但是不能直接訪問 IStream。)IStream 接口能夠有效地訪問一個無結構的字節緩沖區。向流中寫入數據的代碼和讀取數據的代碼必須了解放在流中的數據的格式,如圖 2?所示。

使用流傳輸數據的優勢是所有運行基于 Win32 操作系統的機器都將擁有流封送處理代碼。但是如?圖 2?所示,通過 IStream 接口不能直接訪問流中的數據。如果流中包含了很多數據項,就需要多次調用流來訪問其中的數據。

返回頁首

提高數據傳輸性能

既然我已經解釋了幾種傳輸數據的方法,現在讓我們更詳細地看看性能方面的問題。在一個程序員看來分布式應用程序是很了不起的,因為它們使得您能夠利用網絡上很多機器上的數據和組件功能。Windows DNA 提供了訪問這些分布式組件的平臺和工具。然而,從性能方面來看分布式計算真的存在很多問題。與上下文之內的調用相比,可能需要花費四個數量級的時間完成跨越機器邊界的調用(有關詳細信息,請參見 1997 年 5 月一期?MSJ的ActiveX/COM Q&A專欄)。為了獲得最佳的性能,應當盡量減小網絡調用的次數,并且可能的話予以完全避免。

并不是總能夠避免網絡調用,在某些情況下可能在不知不覺地進行網絡調用。在 Microsoft 事務服務 (MTS) 中的分布式事務就會發生這種情況。MTS 使得能夠在一臺機器上創建一個事務,并且登記其他機器上的資源管理器到相同的事務中。因為一個 MTS 組件的上下文對象包含著組件事務需求(在 MTS 目錄中進行保留)的信息和有關該組件正在使用的任何現有的事務的詳細信息,所以可以這么做。當這樣的一個 MTS 組件通過一個 inproc 資源分配器使用一個資源管理器時,MTS 檢查上下文對象,如果事務存在的話,MTS 就會通知資源分配器在事務中登記資源管理器。如果事務性的 MTS 組件訪問了另一個具有必需的事務屬性的 MTS 組件,事務就會導出到新的組件中。

MTS 在通常的 DCOM 基礎上工作,因此在網絡上有單獨傳遞的數據包來進行組件激活請求和方法調用以及生成維護事務的 Microsoft 分布式事務處理協調器消息。結果,經常可以通過將保持事務的本地化以及避免分布式事務相結合來大大提高 MTS 應用程序的性能。

在 COM+ 中沒有提及的一處改進是通過截獲用于訪問遠程 COM+ 組件的 DCOM 數據包,COM+ 能夠簡化分布式事務的使用。這就使得 COM+ 成為需要分布式事務處理的應用程序的一個更好的平臺。然而,因為 COM+ 使用 DCOM 數據包來傳輸事務 ID,而 MTS 不這樣做,因此兩者不能進行互操作。結果,就不能在同一個事務中使用基于 MTS 的組件和 COM+ 組件。

即使做了這樣的優化,如果一個事務中涉及的一個資源管理器在另一臺機器上創建和協調使用,那么始終需要額外的網絡調用來執行兩相提交。因此,盡可能地保持事務的本地化會很有意義。

如果必須訪問在遠程機器上的組件,首先要確定事務是否必須在本地機器上創建,然后傳遞到遠程組件。如果不是這樣的話,那么從本地 COM+ 組件中刪除事務支持。

通常資源管理器指的是像 SQL Server? 這樣的數據源。通常來說,組件應該盡可能地靠近所使用的數據,因此通常中間層和使用的數據源位于同一臺機器上。如果這樣是不可能的,那么考慮使用存儲過程來操作數據源中的數據。這樣事務就能在存儲過程中創建,從而位于使用它的機器本地。

返回頁首

緩沖區大小和跨機器的調用

保證網絡調用的數量很小是非常重要的,但是保證緩沖區大小盡可能的大也是同等重要的。一般來說這只是個常識。在一個 DCOM 數據包中 RPC 和 DCOM 頭信息占據了大約 250 個字節左右,如果增加在每個網絡調用中傳遞的緩沖區的大小,就能夠確保大多數的 DCOM 數據包包含的是數據而不是協議的開銷。當然,如果緩沖區非常大,幾乎就肯定意味著已經在一個調用中聚合了本該在多個網絡調用中需要發送的數據。


圖?3?傳輸時間與緩沖區大小

在圖 3 中我標出了我的測試結果來表明數據緩沖區的傳輸時間是如何隨著緩沖區大小的改變而改變的。我已經使用了各種通用的傳遞數據的方法,具體描述參見圖 4。測量是通過在一個流量并不繁忙的網絡上運行的 Windows 2000 的兩臺機器上進行數據傳輸而進行的。我認真地包含了釋放數據時客戶端清空所有緩沖區所用的時間。由于使用不同的網絡和機器會獲得不同的數值,因此絕對數值是不重要的,重要的是變化的趨勢。正如能夠看到的,當緩沖區的大小達到 8KB 以后,線幾乎匯合了。換句話說,超過這個點之后數據的傳輸效率都是相同的,而與緩沖區的大小無關。在這個值之下,數據傳輸效率隨著緩沖區大小的減小而大大下降。

另一個重要的發現是除了通過流對象(總是比其他方法花費更長的時間)傳輸數據之外,傳輸速率實際上是相同的。這指出了 Windows 2000 必須使用相似的(如果不是相同的話)封送處理代碼來傳輸 BSTR、SAFEARRAY 以及類似的數組。這對于喜愛 Visual Basic 的程序員來說是個好消息。這意味著不必僅僅因為自動化封送處理不允許它們使用類似的數組來傳輸數據,就使它們無法使用封送處理的過程。.它們現在也能夠在兩臺機器之間高效地傳輸大的緩沖區。

返回頁首

封送處理對象

那么在一個分布式應用程序中應如何在進程之間傳輸數據呢?正如我所提到的,最重要的是設計一些接口以少量的網絡調用傳輸大的數據緩沖區,而不是以大量的網絡調用傳輸少量的數據。

在 Visual Basic 中您習慣使用的屬性訪問類型可能是分布式應用程序中最壞的事情。

Dim day As New Day day.Day = 8 day.Month = 9 day.Year = 2000 Debug.Print day.DayName

如果 Day 對象駐留在另一個上下文中,那么對象的每一次調用都會涉及到封送處理。在這個示例中,對該對象進行了四次調用。將這些屬性用一個簡單的方法代替可以很容易地將其減少為一個調用:

Dim day As New Day Debug.Print day.GetDayName(8, 9, 2000)

對于 MTS 和 COM+ 開發人員來說,用這種方式訪問對象是非常熟悉的。因為組件的狀態是在方法的參數中進行傳遞的,所以組件的類型通常被稱為是無狀態的。通過這種方式訪問 MTS 和 COM+ 事務性組件可以保證事務是隔離的,激活組件只是為了執行 GetDayName。
應該盡可能地通過傳值的方式傳遞數據,而不是通過引用的方式。對象是非常棒的,它們使得代碼更容易閱讀。COM 組件的一個缺點(在分布式數據傳輸這方面)是它們總是通過引用的方式傳輸的。因此,當在遠程機器上創建一個組件的時候,它將總是存活于那個特定的機器,所有對它的訪問都將通過一個經過封送處理的接口指針進行。因此對組件的這種方法調用總是會涉及到一個網絡調用。

當設計您的對象模型時,應該避免使用組件傳遞數據。例如,下面的 Visual Basic 代碼就不是一個好辦法:

Dim person As New Person ' if this is in-context then property access is OK person.ForeName = "Richard" person.SurName = "Grimes" Dim customers As CustomerList Set customers = CreateObject("CustomerSvr.CustomerList", _ "MyRemoteServer") customers.Add person

在此示例中,我假定稱為 person 的對象是在上下文之內創建的,因此我能夠使用屬性訪問來調用它。然后將該對象傳遞給一個遠程的對象:customers。該代碼是可讀的,也是有邏輯性的。現在正在向客戶列表中增加一個新的人員,因此創建一個 Person 類的新實例,然后將其加入到 CustomeList 類的一個實例中。然而,這個代碼對于分布式應用程序來說是很糟糕的,因為 person 對象不是直接傳遞給 customer 對象的,而是通過引用來完成的。這就意味著 customer 對象必須進行網絡調用來從 person 對象中獲得數據。在這個簡單的示例中,如果 CustomerList 類有一個方法,能夠向其傳遞客戶的名稱,而不是使用一個附加的對象,那么結果會好得多。

當然,實際中的代碼很少能夠如此簡單。傳遞對象確實是有優勢的,尤其是在對象有很多數據成員的情況下。您是否曾經調用一個具有 10 個參數的方法,并且獲得 E_INVALIDARG ,然后花費很多時間試圖確切地發現哪個參數是不合法的以及為什么會這樣?如果將數據作為屬性傳遞給上下文之內的對象,那么這種情況是可以避免的。這樣對象就能夠在每個屬性改變的時候執行驗證,如果屬性是無效的,那么這就使得對象能夠返回一個有意義的錯誤代碼。為了獲得通過對象傳遞數據的優勢 (而又不具有沒有跨越上下文訪問的低效性)實現該對象,因此要通過傳值的方式封送處理的。

返回頁首

通過傳值的方式進行封送處理

關于傳值的方式進行封送處理以前已經在?MSJ討論過,但是我會簡略地概述,因為后面我會更深入地討論封送處理。(要想獲得更好的入門知識,請參見 1999 年 3 月一期的?MSJHouse of COM?專欄)如果一個組件想要參與封送處理機制,它應當實現 IMarshal。當 COM 創建一個組件時,它將總是查詢這個接口。如果組件不實現 IMarshal 接口,這就意味著它愿意使用標準的封送處理。如果組件實現了 IMarshal,那么 COM 將調用其方法來獲得在客戶端上下文中使用的代理對象的 CLSID,并且獲得包含了將要傳遞到代理的信息的數據塊,從而能夠連接到那個對象。

在通過傳值方式的封送處理中,一個組件指明它將總是在上下文之內進行訪問。這是通過使 COM 在客戶端上下文中創建一個組件的克隆來實現的。為了做到這一點,組件必須能夠序列化其狀態并且根據這個序列化的狀態初始化自己的一個副本。當 COM 封送處理組件的接口時,它請求代理對象的 CLSID。然后組件就能夠返回自己的 CLSID 來強制 COM 在客戶端的上下文中創建一個組件的未初始化的版本。當 COM 通過調用 IMarshal::MarshalInterface 請求組件提供封送處理信息時,組件應當將其狀態序列化到封送處理的數據包中。COM 然后將這個數據包傳遞給代理對象(客戶端上下文中未初始化的組件實例),代理對象能夠從中提取出組件狀態信息并且使用這個信息初始化克隆。通過傳值的封送處理機制基本上凍結了對象,將其復制到客戶端上下文中,然后在那里重新生成組件。到上下文之外的對象的連接就不再需要了,因為代理是上下文之內的版本,并且所有的 COM 調用都得到它提供的服務。

通過傳值封送處理比想像的要更常用的多。ActiveX 數據對象斷開連接的記錄集就是傳值封送處理的一個眾所周知的例子。標準的錯誤對象(通過 CreateErrorInfo 創建,并通過 GetErrorInfo 訪問)也是通過傳值封送處理的,因此當客戶端代碼訪問錯誤對象來獲得關于錯誤的信息時,調用將不會涉及到封送處理。然而值得注意的是,OLE DB 使用的擴展錯誤對象不是通過傳值封送處理的。相反,它們在客戶端使用稱為 lookup 對象的附加對象調用 IErrorInfo::GetDescription 的時候來生成錯誤描述,lookup 對象運行在產生錯誤的對象上下文中。這需要一個封送處理調用。

應當注意的是通過傳值的封送處理組件強加了一種限制條件。如果到上下文之外的組件的連接丟失了,那么代理就不能向那個組件中寫入值了,客戶端接收到的代理就是只讀的。

返回頁首

處理程序封送處理

在 COM 規范中處理程序封送處理描述為在標準和自定義的封送處理之間的一種中間方式。即開發人員使用標準的封送處理機制來提供額外的代碼,但是本質上是保證了體系結構的完整性。

處理程序封送處理并不是什么新概念。它首先是作為 OLE 2 中的一部分出現的,其中它被用于復合文檔中的嵌入對象。OLE 2 的問題之一是當加載多于一個的 OLE 服務器時,由于內存消耗較大,整個系統就會逐漸停頓。inproc 處理程序緩解了這個問題,因為這些處理程序能夠實現一些對象的接口和方法(例如,呈現),它們能夠被 inproc 代碼執行。如果客戶端請求一個處理程序不能執行的操作,那么處理程序能夠加載服務器,并且使它來完成該工作。

處理程序封送處理的一種形式能夠在 Windows 2000 之前的各 Windows 版本上實現。組件能夠實現 IMarshal 來指明應該使用稱為處理程序的自定義代理對象,用其代替標準的封送處理對象。當 COM 通過調用 IMarshal::MarshalInterface 來請求組件獲得封送處理數據包的時候,它通過調用 CoGetStandardMarshal 獲取一個標準的封送處理數據包。這就意味著對象的接口是通過使用標準封送處理進行封送處理的,因此開發人員不必擔心編寫進程之間的通信代碼。組件實現 IMarshal 的主要原因是,這樣做它能夠使用 GetUnmarshalClass 來返回處理程序對象的 CLSID。但是,組件和處理程序能夠利用 IMarshal 正在使用的事實,能夠將額外的初始化數據追加到封送處理數據包中。

既然組件的接口使用標準的封送處理,處理程序就能夠訪問上下文之外的對象,但是它也能以本地的方式處理對象的一些接口方法。所以,一個枚舉器可能實現 Next 方法從緩存中返回數值,并且通過調用請求大量數據項的實際對象來補充該緩存。.但是,如果實現 IMarshal 的唯一目的是指明要使用的自定義代理的 CLSID 的話,那么在對象中實現 IMarshal 的所有方法就是不必要的。

COM 提供了另外的方法,其中對象不需要實現 IMarshal。相反,它實現稱為 IstdMarshalInfo 的一個接口,如下面的代碼所示,其中唯一的一個方法稱為 GetClassForHandler,它和 GetUnmarshalClass 是等價的。

[ local, object, uuid(00000018-0000-0000-C000-000000000046) ] interface IStdMarshalInfo :IUnknown { HRESULT GetClassForHandler([in] DWORD dwDestContext, [in, unique] void *pvDestContext, [out] CLSID *pClsid); }

COM 會根據 CLSID 注冊表項查找 CLSID,以發現帶有到實現處理程序的服務器的路徑的 InProcHandler32 的項。

Windows 2000 中的處理程序封送處理使得能夠與客戶端的封送處理進程進行掛鉤。通過允許處理程序判斷一個封送處理調用是否是必要的,您可以利用這一點限制對組件調用的數量。處理程序應當實現組件中允許客戶端調用的接口。如果客戶端查詢了一個處理程序沒有實現的接口,那么調用就會失敗。


圖?5?處理程序封送處理體系結構

圖 5 .給出了客戶端的體系結構。正如所看到的,處理程序是通過實現了 IUnkown 接口的一個客戶端標識對象來聚合的。處理程序能夠選擇在其實體中實現接口或者它可能決定將客戶端的調用委托給實際的對象。在后一種情況,處理程序應該獲得一個到代理管理器的指針,并且利用那個指針來訪問對象接口。為了做到這一點,處理程序調用:

HRESULT CoGetStdMarshalEx(IUnknown* pUnkOuter,DWORD dwSMEXFlags, IUnknown** ppUnkInner);

第一個參數是處理程序用于控制的 IUnknown — 標識對象。第二個參數是個標志,用于指定是否需要代理管理器或者服務器端標準的封送拆收器。處理程序傳遞一個 SMEXF_HANDLER 值。如果調用成功的話,代理管理器的一個指針就會在最后的參數中返回。處理程序然后就能查詢所需的接口的指針,將返回一個標準接口代理的指針。既然這是一個到標準封送處理的一個掛鉤,接口可能是自定義的或者雙重接口。

圖 6?給出了訪問一個字符串數組的接口的處理程序,其中數組中的字符串是一個文件夾中包含的文件的名稱。這些代碼來自于 FileEnum 示例,可以從本文開始部分的鏈接中下載。


圖 7 在 FileEnum 中使用的對象

圖 7 給出了在本例中使用的對象。客戶端的上下文使用下面的代碼來實現:

Interface IFiles2 :IDispatch { HRESULT GetNextFile ([ out, retval] BSTR:pData); };

而服務器端的上下文對應于下面這個代碼片段:

interface IFiles :IUnknown { HRESULT GetNextFiles ([ in] ULONG count, [out, size_is(count), length_is(*pFetched)] BSTR* pData, {out} ULONG* pFetched); };

注意處理程序和組件實現了兩個不同的接口。處理程序實現了 IFiles2,它只有一個稱為 GetNextFile 的方法。這樣將會返回組件維護的一個特定文件夾中文件名列表中的下一個文件名。組件實現了 IFiles 接口,該接口已經針對網絡進行了優化,允許通過 GetNextFiles 方法獲得多個文件名。IFiles 使用一個代理-存根 DLL 進行封送處理,因為它使用了 [size_is()] 和 [length_is()]。IFiles2 是在上下文之內訪問的,因此它不會被封送處理。

IFiles::GetNextFile 通過本地維護一個緩存進行工作,并且當該緩存為空的時候,它會調用 Files 對象來獲得數據項的 BUF_SIZE 數量。這種方案的一個比較煩人的特性是處理程序在客戶端上下文中創建,但卻沒有初始化。因此一旦客戶端在其上下文中激活了處理程序,上下文之外的調用必須在第一次客戶端訪問的時候進行。

一個更有效的方案是向處理程序傳遞一些初始化值。在 Windows 2000 中的處理程序封送處理允許這樣做,但是對象和處理程序必須實現 IMarshal。對象必須提供除 IMarshal::UnmarshalInterface 之外的所有方法的實現,因為這是處理程序必須實現的唯一的方法。對象能夠使用 IMarshal::MarshalInterface 來獲得對封送處理數據包的訪問,并且插入自己的數據,這很類似于通過傳值的封送處理,當 COM 調用 IMarshal::GetMarshalSizeMax 時會指定數據的大小。但是對象是如何訪問封送處理數據包的呢?這又需要調用 CoGetStdMarshalEx:

CComPtr m_pMarshal; CComPtr m_pUnk; HRESULT FinalConstruct() { HRESULT hr; hr = CoGetStdMarshalEx(GetUnknown(), SMEXF_SERVER, &m_pUnk); if (FAILED(hr)) return hr; hr = m_pUnk->QueryInterface(&m_pMarshal); if (SUCCEEDED(hr)) Release(); return hr; }

這段代碼將對象的 IUnknown 接口作為控制 unknown 傳遞給 CoGetStdMarshalEx,并且把 SMEXF_ SERVER 作為 dwSMEXFlags 參數傳遞。這個標準的封送處理對象將 AddRef 這個指針。既然這樣代表了一個額外的引用,調用代碼將調用 Release 來考慮這一點。接下來,代碼會查詢 IMarshal。值得注意的是 IMarshal 和 IUnknown 指針必須被緩存處理。如果在 FinalConstruct 后面釋放了 IUnknown 指針,那么 IMarshal 接口就會無效。

此后,IMarshal 指針就能夠用于實現對象上的 IMarshal,如圖 8?所示。這里我假定想要封送處理到處理程序的數據位于一個稱為 ExtraData 的緩沖區中,緩沖區的大小是 DATA_SIZE 字節。注意 GetUnmarshalClass 是由標準封送拆收器實現的。這就意味著不管標準封送拆收器認為使用什么樣的接口封送拆收器進行封送處理,它都會用于跨越上下文的調用。您的接口可能用任何的方式進行封送處理,包括類型庫封送處理,因此客戶端可以是腳本客戶端。

在客戶端,代碼應當至少實現 IMarshal::UnmarshalInterface 接口,如圖 9?所示。除非試圖將代理指針封送處理到另一個上下文,否則將不會調用其他的方法。(為了處理這種情況,只需要把這些方法委托給標準的封送拆收器。既然已經指定了處理程序,標準的封送拆收器就會在新的上下文中加載它。)

聚合的標準封送拆收器(從 CoGetStdMarshalEx 返回)只是在 Windows 2000 系統上可用,因此處理程序不會運行在任何其他的操作系統上。然而,如果對象實現了 IStdMarshalInfo,則即使運行的不是 Windows 2000,關于處理程序的信息也將傳回給客戶端機器,但是將會得到一個錯誤代碼。既然不能關閉處理程序封送處理,那么客戶端和服務器必須運行在 Windows 2000 上。

返回頁首

使用管道傳輸數據

假想您有幾 MB 甚至幾 GB 字節的數據需要傳輸。數據包將遠遠大于 8 KB,所以不必擔心效率低下的網絡調用,但是需要注意其他的事情。考慮進行一次調用并且處理結果的情況。首先客戶端調用組件,請求返回數據。組件必須從某個地方獲得數據,并且將其復制到 RPC 傳輸的緩沖區中。RPC 跨越網絡傳輸數據,并且將其復制到在客戶端上下文中的一個緩沖區內。一旦將其復制到緩沖區中,客戶端就能夠訪問數據。在此期間,客戶端線程會堵塞。

這時客戶端線程能夠處理數據,但是必須記住這是大量的數據,因此需要花費很長的時間。在這個處理時間內,組件實際上是空閑的 — 就客戶端而言。顯然,需要花費很長時間生成數據并且傳輸數據,而客戶端卻在等待。

因此開發了 COM 管道來減少等待的時間。隱藏在后面的思想是要傳輸的數據緩沖區應當分割成數據塊,然后一個接一個地在管道中傳輸。客戶端不是花很長時間等待獲得整個的緩沖區,而只需等待較短的時間獲得到達的較小數據塊。一旦客戶端獲得了緩沖區,就能夠開始對它進行處理。COM 然后請求從組件發送的另一個數據塊,即使客戶端還沒有請求它。在一個數據塊在處理的時候請求另一個數據塊的過程稱為預讀。

如果恰當地進行平衡的處理,花在處理一個數據塊的時間和花在生成以及傳輸另一個數據塊的時間相同。這就意味著客戶端可立即訪問下一個數據塊,而不用等待。當然,這種平衡不是很容易就能達到的,但是所獲得的時間的節省是非常重要的。

道并不是新的技術,Microsoft RPC就已經支持它一段時間了。其中的不同在于在 RPC 中,必須定義要通過管道傳輸的數據,因為 RPC 不是基于對象的,必須處理上下文句柄。Windows 2000 Platform SDK 定義了三種管道接口:IPipeByte、IPipeLong 和 IPipeDouble(參見圖 10)。每個運行 Windows 2000 的機器都有封送拆收器。這些接口的區別只在于它們傳輸的數據的類型不同。

每個管道接口有兩個方法:Push 和 Pull,這就意味著 COM 管道是雙向的。一旦一個可執行程序擁有從另一個程序中獲得的管道,它就能夠接收 (pull) 和傳輸 (push) 數據。實際上,同時可以完成這兩件事情!需注意這些接口通過 async_iid 屬性聲明,因此能夠同步或者異步(不阻塞)地進行調用。過一會兒我將會回過頭來討論這個問題。

當您使用管道的時候,必須做出的第一個決定就是應用程序中的哪一部分將實現管道代碼,是客戶端還是組件。考慮這兩種方法:

HRESULT ProvidePipe([in] IPipeByte* pPipe); HRESULT GetPipe([out] IPipeByte** ppByte);

第一個方法設計為只有實現了 IPipeByte 接口的客戶端才可以調用。它創建一個這樣的實例,并且將其傳遞給組件,組件然后初始化調用,來接收或者傳輸數據。對于第二種方法,組件在服務器上下文中訪問管道的實現,在這種情況下,客戶端而不是組件發出接收或者傳輸操作。

接收和傳輸數據是非常簡單的。您的代碼不必關心預讀的特性,因為這是由 Windows 2000 提供的管道封送拆收器來執行的。但是需要解決一個問題: COM 是如何知道有足夠的數據可用于執行預讀?一個接收操作意味著用于接收操作的代碼必須重復調用 IPipeXXX::Pull,當處理每個緩沖區的時候,COM 會調用組件來獲得下一個緩沖區。顯然,當數據用完的時候必須告訴 COM,這樣就不再進行預讀了。為了做到這一點,管道實現必須在 pcReturned 參數中返回 0。結果,使用 Pull 模式總是多用一次網絡調用。

圖 11?給出了一個簡單的管道實現來通過管道傳輸文本文件。這個組件只是實現了 Pull 方法,該方法會被一直重復調用直到它指出返回的字節數為 0。當文件中不再有數據的時候,管道將返回這個值。這個管道可以通過 GetFileData 方法返回,如圖 12?所示。
Push 方法同樣也很簡單。.Push方法也是重復進行調用直到不再有數據發送為止。然而,COM 知道數據已用完,它不能執行預讀,數據的推送程序必須發送零個字節,如圖 13?所示。

實際的數據傳輸是由管道來執行的,因此當不需要管道的時候,一定要確保通知它,這點很重要。如果忘記調用 Push 并傳遞零個字節,或者實現了 Pull 使得當傳輸完畢時返回零個字節,那么管道將一直處于激活狀態,COM 也會仍擁有對它的引用。如果試圖關閉擁有管道代理的單元,就會看到這一點 — 對 CoUninitialize 的調用將會掛起,直到 COM 調用(如果該調用跨越了機器的邊界)超時。

通過每一個管道調用適宜傳輸的緩沖區大小是多少?這就需要考慮兩個標準。第一是網絡的效率。應當在典型的條件下在目標網絡上執行基本的計時測試,從而了解多大的數據包是最有效率的。(我的網絡如下面圖 3 中所示的結果,對于 8 KB 或者更大的數據包是有效的。)另外一個標準是接收數據的代碼在數據上執行的處理。理想的情況是每個緩沖區的處理花費的時間和生成以及傳輸該緩沖區所花費的時間是相同的。在那樣的情況下,當處理完一個緩沖區之后,COM 會接收下一個緩沖區,這個緩沖區就可用于處理了。

確定最佳緩沖區大小的唯一方法是在目標網絡上測試代碼。圖 14?給出了執行這樣的測試的簡單的類和代碼,但是要記住 Heisenberg 的不確定性原理 — 對系統的測量將會影響系統(在這種情況下,計時將包括用于進行計時的時間)。但是從這個類中,能夠對數據處理應花費的最大時間的概念有所了解。應當針對各種緩沖區的大小進行數據傳輸測試。

測試中的下一步是在客戶端使用靜態數據(換句話說,就是不傳輸任何數據)來測試數據處理需要花費多長時間。同樣,使用各種大小的緩沖區運行測試。最后,比較兩組數字,選擇在處理時間和數據傳輸之間匹配最好的緩沖區的大小。本文的下載版本包括一個項目,使得能夠通過管道讀取和寫入文件內容。

返回頁首

異步管道

管道接口的異步版本如何呢?盡管管道預讀使得您能夠同步數據傳輸和處理,但當數據緩沖區在傳輸的時候,客戶端線程可能阻塞。為了回避這一點,可以使用非阻塞版本的管道接口來調用管道。管道實現者能夠利用非阻塞機制來實現管道,這樣的管道要使用自定義線程池而不是 RPC 線程池來運行管道代碼。這使得管道實現者能夠更有效地管理線程。

當通過非阻塞版本的管道接口接收數據的時候,調用者線程通過調用 Begin_Pull 方法發起數據傳輸,要指明需要傳輸多少數據項。然后它會通過調用 Finish_Pull 方法執行一些其他的處理和返回以獲取數據(以及返回的數據項個數)。這時,COM 會接收數據并且進行緩沖以準備進行收集。當調用 Finish_Pull 的時候,COM 會執行預讀來獲得下一個緩沖區。看一下這個方法的非阻塞版本:

HRESULT Begin_Pull([in] ULONG cRequest); HRESULT Finish_Pull([out, size_is(*pcReturned)] BYTE* buf, [out] ULONG* pcReturned);

這是偽 IDL 因為實際的方法是由 MIDL 生成的。初看起來它很奇怪,因為在實際的傳輸執行之后,調用 Finish_Pull 的時候,傳遞想要填充的緩沖區。可能 COM 會將數據讀入某個私有的緩沖區,然后當調用 Finish_Pull 的時候,它將數據從這個緩沖區中復制到您的緩沖區里。
Push 的非阻塞版本對于不阻塞當前線程向另一個進程發送數據是非常有用的,尤其是如果數據量非常大的時候,就更加有用了。由于 Push 中沒有 [out] 參數,因此應當調用 Push_Finish 使得 COM 清理掉可能已經使用的任何資源,并且判斷傳輸操作是否成功執行了。(Push_Begin 的返回值僅僅表示 COM 接受了方法調用)

要使用管道,必須要有最新的 Platform SDK 的頭文件和庫,并且必須定義 _WIN32_WINNT,使其在 stdfx.h 中具有 0X500 這個值。

返回頁首

總結

COM 上的數據傳輸需要認真選擇在網絡上移動數據的最佳方法。總的說來,應該盡量少地進行網絡調用。當確實要進行網絡調用時,應當使得傳輸數據緩沖區盡可能大,并始終避免在 Visual Basic 中使用的屬性訪問。

為了進一步便于數據傳輸,COM 提供了幾個工具。首先,為了避免有很多參數的方法調用的問題,可以在一個對象中傳輸數據,只要對象是傳值封送處理的即可。這使得能夠同時獲得一個對象提供的驗證的優勢,以及數據通過傳值傳遞時網絡調用的效率。接下來,Windows 2000 允許創建輕量級的客戶端處理程序,這樣的處理程序能夠智能地決定是否調用上下文之外的組件。這樣的一個處理程序能夠緩存結果,并且進行緩存的讀取和寫入。

最后,Windows 2000 提供了管道接口,使得能夠在網絡上高效地傳輸大量的數據。這是通過將數據分割成大小相當的數據塊來實現的,這樣就允許 COM 在管道之上處理這些數據塊的傳輸。?



總結

以上是生活随笔為你收集整理的封送处理您的数据:利用 COM 和 Windows 2000 的高效传输数据的技术的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。