热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

From:http:blog.csdn.netzhoujianheiarticledetails1844319摘要您所选择的数据传输方式在分布式应用程序中是非常

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

摘要 您所选择的数据传输方式在分布式应用程序中是非常重要的。Windows 2000 提供了几种新的特性,可以更加高效地进行数据传输。轻量级的处理程序使得您能够编写智能代理,它们能够缓存结果并且执行带缓冲的读写操作,从而将网络调用的次数减至最小。Windows 2000 也使得您能够使用管道接口通过一种预读的设施来高效地传输大量的数据。

本文阐述了利用这些新特性在 Windows 2000 中提高数据传输效率的几种方法。它同时也报告了传输时间测试的结果,还给出了传输缓冲区大小的推荐标准。

本页内容
封送处理数据 封送处理数据
指定使用 IDL 传输的数据 指定使用 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 所示。

datatransfig01

图 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
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
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
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 数据包包含的是数据而不是协议的开销。当然,如果缓冲区非常大,几乎就肯定意味着已经在一个调用中聚合了本该在多个网络调用中需要发送的数据。

datatransfig03

图 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 中的处理程序封送处理使得能够与客户端的封送处理进程进行挂钩。通过允许处理程序判断一个封送处理调用是否是必要的,您可以利用这一点限制对组件调用的数量。处理程序应当实现组件中允许客户端调用的接口。如果客户端查询了一个处理程序没有实现的接口,那么调用就会失败。

datatransfig05

图 5 处理程序封送处理体系结构

图 5 .给出了客户端的体系结构。正如所看到的,处理程序是通过实现了 IUnkown 接口的一个客户端标识对象来聚合的。处理程序能够选择在其实体中实现接口或者它可能决定将客户端的调用委托给实际的对象。在后一种情况,处理程序应该获得一个到代理管理器的指针,并且利用那个指针来访问对象接口。为了做到这一点,处理程序调用:

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

第一个参数是处理程序用于控制的 IUnknown — 标识对象。第二个参数是个标志,用于指定是否需要代理管理器或者服务器端标准的封送拆收器。处理程序传递一个 SMEXF_HANDLER 值。如果调用成功的话,代理管理器的一个指针就会在最后的参数中返回。处理程序然后就能查询所需的接口的指针,将返回一个标准接口代理的指针。既然这是一个到标准封送处理的一个挂钩,接口可能是自定义的或者双重接口。

图 6 给出了访问一个字符串数组的接口的处理程序,其中数组中的字符串是一个文件夹中包含的文件的名称。这些代码来自于 FileEnum 示例,可以从本文开始部分的链接中下载。

datatransfig07

图 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 在管道之上处理这些数据块的传输。 




推荐阅读
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • C语言注释工具及快捷键,删除C语言注释工具的实现思路
    本文介绍了C语言中注释的两种方式以及注释的作用,提供了删除C语言注释的工具实现思路,并分享了C语言中注释的快捷键操作方法。 ... [详细]
  • Android JSON基础,音视频开发进阶指南目录
    Array里面的对象数据是有序的,json字符串最外层是方括号的,方括号:[]解析jsonArray代码try{json字符串最外层是 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 前景:当UI一个查询条件为多项选择,或录入多个条件的时候,比如查询所有名称里面包含以下动态条件,需要模糊查询里面每一项时比如是这样一个数组条件:newstring[]{兴业银行, ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • 本文介绍了Java集合库的使用方法,包括如何方便地重复使用集合以及下溯造型的应用。通过使用集合库,可以方便地取用各种集合,并将其插入到自己的程序中。为了使集合能够重复使用,Java提供了一种通用类型,即Object类型。通过添加指向集合的对象句柄,可以实现对集合的重复使用。然而,由于集合只能容纳Object类型,当向集合中添加对象句柄时,会丢失其身份或标识信息。为了恢复其本来面貌,可以使用下溯造型。本文还介绍了Java 1.2集合库的特点和优势。 ... [详细]
  • linux进阶50——无锁CAS
    1.概念比较并交换(compareandswap,CAS),是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作࿰ ... [详细]
  • 1Lock与ReadWriteLock1.1LockpublicinterfaceLock{voidlock();voidlockInterruptibl ... [详细]
  • 本文详细介绍了PHP中与URL处理相关的三个函数:http_build_query、parse_str和查询字符串的解析。通过示例和语法说明,讲解了这些函数的使用方法和作用,帮助读者更好地理解和应用。 ... [详细]
  • C++字符字符串处理及字符集编码方案
    本文介绍了C++中字符字符串处理的问题,并详细解释了字符集编码方案,包括UNICODE、Windows apps采用的UTF-16编码、ASCII、SBCS和DBCS编码方案。同时说明了ANSI C标准和Windows中的字符/字符串数据类型实现。文章还提到了在编译时需要定义UNICODE宏以支持unicode编码,否则将使用windows code page编译。最后,给出了相关的头文件和数据类型定义。 ... [详细]
author-avatar
林泳_钿
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有