进程内服务器,进程外服务器,远程服务器

本文深入探讨了COM组件在不同进程间通信的原理,包括EXE与DLL服务器的角色,远程服务器的概念,以及如何实现跨进程接口调用。讨论了本地过程调用的调整机制,IMarshal接口的应用,代理/残根DLL的作用,IDL/MIDL编译器的使用,以及HRESULT值在网络通信中的重要性。此外,还介绍了EXE服务器的实现细节,如类工厂的启动与释放,LockServer的修改,和消息循环的加入。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

EXE 中的服务器

不同的进程

DLL也被称作是进程中服务器, 而EXE则被称作是进程外服务器,远程服务器指的是运行于另外一个不同的机器上的进程外服务器。

对于跨越进程边界的接口,我们需要考虑如下一些条件

  1. 一个进程需要能够调用另外一个进程中的函数
  2. 一个进程需要能够将数据传递给另一个进程
  3. 客户无需关心她所访问的服务器是进程内服务器还是进程外服务器

本地过程调用

调整

对指针的处理就将不同于对整数的处理。 对指针的调整指的是要将指针引用的结构复制到另外一个进程中。 但若此指针是一个接口指针 , 则将不能复制此指针所引用的内存。这里我们看到 , 调整并不是用一条简单的 memcpy 命令就能完成的。

 

为对组件进行调整 , 可以实现一个名为IMarshal 的接口。 在 COM 创建组件的过程中 , 它将查询组件IMarshal 接口。 然后它将调IMarshal的成员函数以在调用函数的前后调整或反调整有关的参数

 

代理/残根 DLL

一个代理 就是 同另 外 一个 组件 行为 相同 的 组件。 代理 必须 是DLL 形式的 , 因为它们需要访问客户进程的地址空 间以便对 传给接 口函数的 数据进行 调整。 对数据的调整只完成了任 务的 一 半 , 组件 还需 要 一个 被称 作是 残根 的 DLL, 以对 从客户传来的数据进行反调整。 残根也将对传回给客户的数据进行调整

IDL/MIDL

IDL 接口定义语言。MIDL 编译器即可生成代理和残根DLL。

 

IDL 接口描述举例

import "unknwn.idl" ; 

// Interface IX
[
	object,
	uuid(32bb8323-b41b-11cf-a6bb-0080c7b2d682),
	helpstring("IX Interface"),
	pointer_default(unique)
]
interface IX : IUnknown
{
	HRESULT FxStringIn([in, string] wchar_t* szIn) ;
	HRESULT FxStringOut([out, string] wchar_t** szOut) ;
} ;

. pointer_default

 

pointer_default 的作用就是告诉MIDL 编译器在没有为指针指定其他属性时如何处理此指针。 pointer_default 关键字具有三个不同的选项:

Ref

将指针当成引用,此时表示此指针将总是指向一个合法的地址

Unique

此类指针可以为空,并且在函数内可以修改它们的值,但不能为之指定别名

Ptr

相应的指针就是C指针,此类指针可以是有一个别名,也可以为NULL,并且其值可以被修改

 

2. IDL 中的输入与输出参数

MIDL 可以使用in 和 out 参数属性对属性及残根代码进行进一步的优化,in 代表仅仅输入,残根代码不需要送回任何值。Out 关键字高速MIDL 相应的参数仅被用来从组件向用户传回有关的数据。,可以同时用in和out

  1. IDL 中的字符串

通过参数在函数中加上一个string 修饰符,MIDL 将知道相应的参数为一个字符串,并且它可以通过查找末尾的空字符而决定其产长度。COM 中对于字符串的标准约定是Unicode,即wchar_t。当然,可以使用在COM 头文件中定义的OLECHAR 或 LPOLESTR。

 

  1. IDL 中的HRESULT 值

MIDL 要求用object 修饰符标记的接口返回HRESULT 值。当连接到一个远程服务 器时, 任何函数调用都可能由于网络传输的问题而失败。因此应该有一种让所有函数都能指示网络错误的方法。最简单的方法就是让所有的函数返回一个HRESULT 值

 

若某个函数需 要返回一个不是HRESULT类型的值,那么可以在相应的地方加上一个输出参 。 在FxStringOut 中, 我们定义了一个输出参数以从组件得到一个返回串。函数FxStringOut将使用 CoTaskMemAlloc来分配返回串所需的内存。客户必须使用CoTaskMemFree来释放这块内存。下面的代码显示了如何使用前面定义的 IX 接口。

        wchar_t* szOut = NULL ;
        hr = pIX->FxStringIn(L"This is the test.") ;
        assert(SUCCEEDED(hr)) ;
        hr = pIX->FxStringOut(&szOut) ;
        assert(SUCCEEDED(hr)) ;

        // Display returned string.
        ostrstream sout ;
        sout << "FxStringOut returned a string:  "
             << szOut
             << ends ;
        trace(sout.str()) ;

        // Free the returned string.
        ::CoTaskMemFree(szOut) ;

IDL 中的import 关键字
用于将其他IDL 文件中的定义包含到当前文件中。

六:IDL 中的site_is 修饰符

客户和组件之间传递数组的接口的IDL 描述


// Interface IY
[
	object,
	uuid(32bb8324-b41b-11cf-a6bb-0080c7b2d682),
	helpstring("IY Interface"),
	pointer_default(unique)
]
interface IY : IUnknown
{
	HRESULT FyCount([out] long* sizeArray) ;

	HRESULT FyArrayIn([in] long sizeIn, 
	        [in, size_is(sizeIn)] long arrayIn[]) ;

	HRESULT FyArrayOut([out, in] long* psizeInOut, 
	        [out, size_is(*psizeInOut)] long arrayOut[]) ;
} ;

提供给 size - is 修饰符的参 数只 能是 输入 参 数或 输入 - 输出 参 数。实际代码实现中,尽量少的使用in,out 属相的参数。

IDL 中的接口

IDL 文件中也可以定义C 和C++ 风格的结构,并可用它们作为函数的参数。

但是在传递包含指针的复杂接口时,MIDL 需要精确制导一个指针所指的到底是什么内容,这样它才能对指针所指的数据进行调整。因此,绝对不可以使用void* 作为参数。如果需要传递一个一般性的接口,可以使用一个IUnknown* 接口。最灵活的方法是让客户传递一个IID,这同QueryInterface 相似。

HRESULT GetIface([in] cosnt IID& iid,[out,iid_is(iid)]IUnknown** ppI);

MIDL 编译器

代理DLL 的建立

MIDL 编译器将为组件生成相应的代理和残根的代码。但是将这些代码编译成一个DLL 仍然是程序员的工作。为此第一步需要为代理DLL 编写一个DEF 文件:

#################################################
#
# Proxy source files
#
iface.h server.tlb proxy.c guids.c dlldata.c : server.idl
	midl /h iface.h /iid guids.c /proxy proxy.c server.idl 

!IF "$(OUTPROC)" != ""
dlldata.obj : dlldata.c 
	cl /c /DWIN32 /DREGISTER_PROXY_DLL dlldata.c

proxy.obj : proxy.c 
	cl /c /DWIN32 /DREGISTER_PROXY_DLL proxy.c

PROXYSTUBOBJS = dlldata.obj   \
                proxy.obj     \
                guids.obj

PROXYSTUBLIBS = kernel32.lib  \
                rpcndr.lib    \
                rpcns4.lib    \
                rpcrt4.lib    \
                uuid.lib 

proxy.dll : $(PROXYSTUBOBJS) proxy.def
	link /dll /out:proxy.dll /def:proxy.def   \
		$(PROXYSTUBOBJS) $(PROXYSTUBLIBS)
	regsvr32 /s proxy.dll
LIBRARY         Proxy.dll
DESCRIPTION     'Proxy/Stub DLL'

EXPORTS
                DllGetClassObject   @1	PRIVATE
                DllCanUnloadNow     @2	PRIVATE
                GetProxyDllInfo     @3  PRIVATE
                DllRegisterServer   @4	PRIVATE
                DllUnregisterServer @5	PRIVATE

代理/残根的登记

编译文件DLLDATA.C 和 PROXY.C 时定义了一个宏REGISTER_PROXY_DLL ,这个宏将生成完成代理/存根的登记所需的代码

然后上面,makefile 文件中,proxy.dll 生成后,紧接着一句,regsvr32 /s proxy.dll 确保了该代理一定会被登记。

登记的位置:

HKEY_CLASSES_ROOT\

                Interfaces\

                                {一个ID}

本地服务器的实现

当运行客户程序时,询问用户想使用组件的进程中服务器版本还是进程外服务器版本。客户将使用CLSCTX_INPROC_SERVER连接进程中服务器,用CLSCTX_LOCAL_SERVER 连接到进程外服务器。

去掉入口点函数

问题:EXE 无法输出函数。现在我们的进程中服务器依赖于如下的一些函数:

DllCanUnloadNow

DllRegisterServer

DllUnregisterServer

DllGetClassObject

 

注册卸载可以通过命令行参数来实现。

EXE 不是被动的,它可以自己实现对于生命周期的控制。

DllGetClassObject的实现相对比较困难。

类厂的启动

CoCreateInstance->CoGetClassObject->DllGetClassObject->返回一个IClassFactory 指针->创建相应的组件。

 

EXE 不输出DllGetClassObject 。COM 维护了一个关于被登记的类厂的内部表格。当用户调用CoGetClassObject->根据CLSID->类厂->如果类厂不在当前的表格->在注册表中查找并启动相应的EXE。此EXE 将完成相应类厂的登记(CoRegisterClassObject),COM->此时可以找到。对于CFactory ,我们可以给它加上一个新的静态成员函数StartFactories,它将为CFactoryData 结构中所列的每一个组件调用CoRegisterCalssObject。

HRESULT CoRegisterClassObject(

  REFCLSID  rclsid,

  LPUNKNOWN pUnk,

  DWORD     dwClsContext,

  DWORD     flags,

  LPDWORD   lpdwRegister

);

上面我们看到, 类厂的登记实际上是一个比较简单的过程: 只需建立相应的类厂并将
其接口指针传给CoRegisterClassObject 即可。CoRegisterClassObject 的大多数参数的含义
都可以从前面的代码明显地看出来。第一个参数是被登记的类的CLSID, 其后是一个指
向其类厂的指针。最后一个指针可以传回一个魔饼。此魔饼将在用CoRevokeClassObject
来取消相应类厂的登记时用到。第三个和第四个参数都是一些DWORD 标志, 它们可以
对CoRegisterClassObject 的行为进行控制

二:类厂的释放

HRESULT CoRevokeClassObject(

  DWORD dwRegister

);

 

下面的星号中间,是一个小细节的地方

**********************************************************************************************************

  1. CoRegisterClassObject 标志

Flags 参数表示EXE 的单个实例能否支持一个组件的多个实例。

 

假定有一个登记了若干个组件的EXE,并且此EXE 需要使用它登记的某一个组件,如果按照:

Hr = ::CoRegisterClassObject(clsid,pIUnknown,CLSCTX_LOCAL_SERVER,REGCLS_MULTI_SEPARATE,&dwRegister);

 

系统将装载EXE 的另外一个实例以提供它自己的组件。这是没有必要和低效的。

此时我们可以将第三个参数设置为:CLSCTX_LOCAL_SERVER|CLSCTX_INPROC_SERVER

 

因为这种情况十分常见,系统提供了REGCLS_MULTIPLEUSE,此标志可以在出现了CLSCTX_LOCAL_SERVER时自动设置CLSCTX_INPROC_SERVER ,因此下面的代码前面的调用是相等同的。:

Hr = ::CoRegisterClassObject(clsid,pIUnknown,CLSCTX_LOCAL_SERVER,REGCLS_MULTIPLEUSE,&dwRegister);

 

为此可首先用如下的命令取消进程中服务器的登记。这将保证可用的服务器只是本地服务器。然后可运行客户程序并选择第二个选项以运行本地服务器。本地服务器此时将能够正常工作。

 

然后我们可以将CFactory::StartFactories中的REGCLS_MULTIPLEUSER 修改为REGCLS_MULTI_SEPARATE,在重新编译、链接客户和服务器之后,可运行客户并选择第二个选项。此时Create调用将会失败,因为没有进程中服务器来满足内部组件的创建过程,而REGCLS_MULTI_SEPARATE则告诉COM 库不要自己来满足对进程中组件的请求。

**********************************************************************************************************

对LockServer 的修改

 

进程中服务器将输出函数DllCanUnloadNow。COM 库将调用此函数以决定能否从
内存中将服务器卸载掉。此函数是通过静态函数CFactory ∷CoUnloadNow 实现的。
CFactory∷CoUnloadNow 将检查静态变量CUnknown∷ s_ActiveComponents 的值, 决定
是否可以将服务器从内存中卸载掉。在建立一个新的组件时, 静态变量CUnknown∷s_ActiveComponents 的值将被相应地增大。但是在创建类厂时我们将不增大s_ActiveComponents的值(因为,组件的激活数目和类厂的创建是无关的)。因此, 一个服务器可以拥有打开的类厂, 但它将仍然能够被关闭掉。

本地服务器所完成的第一件事情就是创建它所用的所有类厂, 而最后做的就是释放这些类厂。若
服务器在它被卸载之前需要等待所有这些类厂被释放, 那么此等待的时间将是相当长的,
这是因为服务器在卸载时需要亲自将它们卸载掉。因此, 当客户在创建组件时, 若它们需
要服务器确实是在内存中, 那么可以使用IClassFactory∷LockServer 来保证这一点。

DLL 不能控制它们的生命期, 它们的装载和卸载都是由另外的EXE 文件完成的。但EXE 则不同,
它们本身可以起到控制作用, 并且可以装卸或卸载它们自己。正是由于EXE 的卸载是由
它自己完成的, 因此需要对LockServer 进行修改以便当加锁计数值变成零时退出相应的
EXE。为此, 我们可以在CFactory 中加上一个新的成员函数CloseExe, 它将给应用程序
的消息循环发送一条WM_QUIT 消息


//
// Determine if the component can be unloaded.
//
HRESULT CFactory::CanUnloadNow()
{
	if (CUnknown::ActiveComponents() || IsLocked())
	{
		return S_FALSE ;
	}
	else
	{
		return S_OK ;
	}
}


// LockServer
HRESULT __stdcall CFactory::LockServer(BOOL bLock) 
{
	if (bLock) 
	{
		::InterlockedIncrement(&s_cServerLocks) ; 
	}
	else
	{
		::InterlockedDecrement(&s_cServerLocks) ;
	}
	// If this is an out-of-proc server, check to see
	// whether we should shut down.
	CloseExe() ;  //@local

	return S_OK ;
}




#ifdef _OUTPROC_SERVER_
	//
	// Out-of-process server support
	//

	static BOOL StartFactories() ;
	static void StopFactories() ;

	static DWORD s_dwThreadID ;

	// Shut down the application.
	static void CloseExe()
	{
		if (CanUnloadNow() == S_OK)
		{
			::PostThreadMessage(s_dwThreadID, WM_QUIT, 0, 0) ;
		}
	}
#else
	// CloseExe doesn't do anything if we are in process.
	static void CloseExe() { /*Empty*/ } 
#endif

组件的析构函数中,也需要调用CloseExe,因为此时组件也可以决定是否可以将其从内存中卸载掉:




//
// Destructor
//
CUnknown::~CUnknown()
{
	::InterlockedDecrement(&s_cActiveComponents) ;

	// If this is an EXE server, shut it down.
	CFactory::CloseExe() ;
}

消息循环

为了不致使EXE 退出,可以加上一个windows 的消息循环。这个消息循环同所有的windows 程序所用的消息循环是相同的。

那么我们如何决定用户何时启动了服务器而不是COM 库呢? 当CoGetClassObject
装载本地服务器的EXE 时, 它将在命令行上加上一个Embedding 参数。EXE 将检查命
令行中的Embedding 参数。若没有此参数, 它将增大s_cServerLocks 值并创建一个窗口
以便同用户进行交互。

当用户将服务器关闭时, 客户可能仍然在使用服务器。因此, 服务器应在用户退出时
将其用户界面元素清除掉, 但它不应该退出, 这样才能继续为其客户提供相应的服务。因
此当服务器收到一个WM - DESTROY 消息时, 它只应在CanUnloadNow 返回S - OK 时
才发送WM - QUIT 消息。

 


	case WM_DESTROY:          // message: window being destroyed
		if (CFactory::CanUnloadNow() == S_OK)
		{
			// Only post the quit message, if there is
			// no one using the program.
			::PostQuitMessage(0) ;
		}
		break ;
  1. 用户的计数

服务器的用户也是一种类型的客户,也需要有它们自己的加锁计数值。当所有用户创建组件时,也需要将CFactory::s_cServerLocks增大。这样,只要有用户在使用服务器,它将一直保存在内存中。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值