组件技术的本质COM实例分析一

发表于:2007-06-30来源:作者:点击数: 标签:
COM编程 进程内COM服务器(In-Process COM Server) 进程内的COM服务器(In-Process Com Server)在这前已经简单的介绍过,在此,只对一些需要明白、理解的知识点进行阐述,再进行编程可能会更好一点;进程内服务器是由于它们在DLL内实现而获得这个名称的。因此
COM编程

进程内COM服务器(In-Process COM Server)

进程内的COM服务器(In-Process Com Server)在这前已经简单的介绍过,在此,只对一些需要明白、理解的知识点进行阐述,再进行编程可能会更好一点;进程内服务器是由于它们在DLL内实现而获得这个名称的。因此,服务器占擗了和使用它的应用程序一样的地址空间(进程)。所有的进程内COM服务器输出四个标准函数:DllRegisterServer、DllUnregisterServer、DllGetClassObject和DllCanUnloadNow。根据字面意思,我们也已经可以感觉的出这些函数的工作都会是什么。当然,Delphi已经为我们提供了这些函数的缺省实现。因此,读者不必自己写代码来实现这些函数,但应该理解他们具体是做什么的。

² DllRegisterServer。DllRegisterServer以两种方式自动调用。IDE的Register ActiveX Server菜单选项调用它,Windows的命令行应用程序RegSvr32.exe(或者Borland应用程序TRegSvr)也会调用它,很多情况下可以直接用Register ActiveX Server去调用,如果手动的去实现,可以写一个.Bat文件。无论通过那种方式调用它,DllRegisterServer用Windows注册表来注册COM对象。

² DllUnregisterServer。可以感觉的出来,这个函数和DllRegisterServer是作相反的工作,实际上它们就是一个相逆的过程,它移走了DllRegisterServer放在Windows注册表中的所有条目。可以用IDE里的 UnRegister ActiveX Server来调用这个工程。

² DllGetClassObject。DllGetClassObject负责提供给COM一个类厂,该类厂用于创建一个COM对象(在讨论类厂的时候,我们也做过明确的说明)。

² DllCanUnloadNow。COM负责调用DllCanUnLoadNow来看是否可以从内存中卸载COM服务器。

线程支持(Threading Support)

线程支持只适合于进程内服务器,并且不适用于进程外服务器。进程内服务器可以附着在几个的一个。进程内服务器的线程模型被存在Windows注册表中,具体如下:

² ……

² ……

² ……

² ……

…………

注册服务器(Registering the Server)

此处将不再重提如何注册服务器,只是简单的说一说为什么要进行注册。我们都知道,普通的Dll也需要进行注册才可以运行,而COM对象或是COM服务器要提供给服务于Client,那么客户端首先要知道要没有这个服务?如何进行这个服务的调用或是访问,因此它会找一些有用的键值,而客户所找的范畴就是注册表。由此而言,注册的确很有必要而肯是必不可少的。

构造函数

应该明确的是,做为一个提供服务的对象或接口或是一个组件,有一步工作应该提前做,那就是构造,而构造也是初始化,并且之前我们也说过,COM对象最好派生于TcomObject类,如此一来,我们就不得不去考虑在TcomObject中定义的构造函数都调用了虚方法函数Initialize。如果需要为自己的COM对象提供初化代码,只需要重载Initialize方法,定义如下:

Procedure Initialize ;Virtual;

将初始化代码放在Initialize中而不是构造函数中的原因是:Delphi中的COM对象的基类包含了一系列的非虚构造函数。根擗需要,类厂奖在不同的实例中调用不同的构造函数。而Initialize是虚方法,并且是唯一可以在调用时不考虑哪个构造函数是用于创建COM对象的方法。

创建一个进程内COM对象的实例

当用户需要在自己的客户程序代码中创建一个COM对象,一般会调用CreateConObject函数,该函数在ComObj.pas中定义,声明如下:

Function CreateComObject(const ClassID:TGUID):IunKnown;

CreateComObject将要创建的COM对象的GUID作为一个参数,并返回该COM对象的IunKnown指针。苦所请求的GUID在Windows注册表中找不到的话,将会抛出一个异常。那么我们一起来分析一下CreateComObject函数的实现如下:

function CreateComObject(const ClassID: TGUID): IUnknown;

begin

OleCheck(CoCreateInstance(ClassID, nil, CLSCTX_INPROC_SERVER or

CLSCTX_LOCAL_SERVER, IUnknown, Result));

end;

之前我们曾讨论过OleCheck,现在将重点的精力放在CoCreateInstance上边。如果根踪CoCreateInstance会发现如下信息:

function CoCreateInstance; external ole32 name @#CoCreateInstance@#;

因此可以这样认为,CreateComObject函数所作的只是调用了一下Windows的函数CoCreateInstance,并提供了一些默认的参数

CoCreateInstance主要有五个参数:

Clsid:Clsid是我们想要创建的COM服务器的GUID。这是我们专们传递给CreateComObject唯一的一个参数。

UnkOuter:只有在此COM对象是集合的一部分时才使用它。

DwClsContext:dwClsContext决定了读者想要创建的类型。CreateCOMObject自动要求创建一个进程内服务器(CLSCTX_INPROC_SERVER)或本地的进程外服务器(CLSCTX_LOCAL_SERVER)。有时与此函数一起使用的另外一个标记是CLSCTX_REMOTE_SERVER。此标志在DCOM中使用,以后将会讨论。

Iid:iid是我们想要获取一个对它的引用的接口。Delphi通常需要对IunKnown接口的应用,因为如果要其它别的引用的话,大多数类厂会失败。Microsoft用此参数主要是为了将来的扩展。

Pv:主要是得到IunKnown接口的指针。

{一个需要注册的是:CoCreateInstance内部创建负责创建COM对象类厂的实例,然后使用类厂来创建对象。创建完COM对象之后,类厂就被销毁。显然,如果要创建相同COM对象的多个实例,这不是非常有效的,在这种情况下就要自大闯将一个类厂的实例,并在删除它之前使用它的CreateInstance方法来创建COM对象。}

正如以前提到的,CreateComObject通常返回一个IunKnown指针。要获取需要的接口针,应使用as操作符,如:

MyIntf := CreateCOmObject(CLSID_MyServer) as IMyInterface



实例:一个简单的COM应用程序

在讨论了COM服务器的一些基本概念之后,我们用一个简单的小实例来说明如何调用以DLL形式提供服务的COM服务器,之后,我们会对此COM服务器进行扩展,并且会再给一个COM服务器的高级编程,但一切需要一步一步的来。

实例说明:

此实例是当正确登录到COM服务器时,就可以实现一个简单的算法,此处没有给出COM服务器与数据库服务器之间的连接,所以登录不是一个动态的从数据库用户信息表里进行判断,而是在程序中指定了一个固定的用户名,当然,在此处,可以连接一些文本数据库来进行动态的判断。此处的算法很简单,就是传统的小猴子吃桃子的问题,一个小猴子有一些桃子,每天吃一半,因为嘴馋,又多吃一个,这样每天就是吃一半多一个,当到了第十天时,吃完之后仅仅有一个了(此处的吃完仍然是吃了一半多一个),而算法就是要知道小猴子子本来要多少只桃子。

问题分析:因为需要用到COM服务器的一些方法,所以可以暂时作如下的决定:

用户的信息判断,即登录是否成功让服务器来为我们判断,客户端仅仅是作简单的错误性检查和数据的提交,正真的实现一个瘦客户端;

{说明:此处所说的瘦客户端希望读者朋友们不要仅仅根据字面意思来看客户端,并非是客户端的代码越少越好,客户端所要作的工作都让服务器去做才是瘦。其实,不应该如此去理解,而我们一般理解的服务器/客户端应该是服务器帮客户端处理一些逻辑上的、业务上的分析,而一些判断性的错误可以由客户端去完成,虽然是客户端,但它同样有一些自身的检查在里边,虽然是服务器端,它仍有一些任务由客户端去完成,再更多的情况,也许对读者朋友来说,感觉可能让客户做的更好}

算法实际上很简单,可以利用循环,也可以利用递归,在此仅仅做简单的分析。我们用循环来处理的话应该需要从后边入手,第十天刚刚吃的还有一个桃子,那么第十天,就应该是四个,第九天是十个,第八天是二十二个……,如此类推,我们可以很容易的得出一共有多少个桃子,可以进行如下的处理:

Var

Acount : Integer;

Asum : Integer;

Begin

Asum := 1;

For Acount := 10 DownTo 1 do

Asum := ( Asum + 1 ) * 2;

End;

理解起来很容易,到了第十天吃过之后还剩一个桃子,那么,第十天实际上应该是四个,哪此类推的倒循环回去就可以知道答案。而得用递归将更简单,如下:

Function Cal(Avalue : Integer) : Integer;

Begin

If Avalue = 11 then

Result := 1 else

Result ;= (Cal(Avalue + 1) + 1) * 2;

End;

猴子吃桃,虽然题目相当简单,但或多或少的有些饶人,用循环我们已经很容易的作了出来,接正是来用递归完成这个过程,应为第10天吃完之后还剩1个,相当于到了第11天还有4个桃子,所以我们就可以列出:if DayValue = 11 then Sum = 1 ;递归的过程很简单,只要我们可以把握住它是一个堆的过程,进去了总还是要出来的,而且先进后出;

1. 创建COM服务器

要创建一个COM服务器来实现此算法,而且,这个COM服务器同时还要做用户登录接口。Delphi7为我们提供了很好的创建向导,使得我们不在如在Delphi3下一样,需要手动键入代码,需要说明的一些问题,因为这个应用程序将作成进程内服务,所以首要的前提就是做一个Library,一切操作都应该在这个Library的容器里进行。

新建一个应用程序(File -> New -> Other)选择ActiveX标签,如下图所示:又击ActiveX Library或是选中ActiveX按回车键,Delphi会为我们自动的完成一些工作。



当点击“OK”按钮时,如下:

library Project2;



uses

ComServ;



exports

DllGetClassObject,

DllCanUnloadNow,

DllRegisterServer,

DllUnregisterServer;



{$R *.RES}



begin

end.

这样就创建了一个进行内COM服务器的首要工作,DLL如上边代码所示,Delphi会为我们自动的建立,而DllGetClassObject、DllCanUnloadNow、DllRegisterServer、DllUnregisterServer在前边的章节我们已经详细的介绍过,在些不再阐述。这后请保存这个文件为SrvPro.

在此稍微的做一下分析,为了实现上边的算法,我们需要用到接口来供客户端调用,而现在我们仅仅完成了ActiveX Library库,接下来的工作就是创建正真的COM组件。创建COM组件也很简单,根据相导提示(File-> New -> Other -> ActiveX),如下图



在此我们简单的来说一说这个Com Object对话框的具体含义:

Class Name :在此填写类名,Delphi会自动的给我们加上“T”;

Instancing : 在进程内服务,它是没有意义的,因为COM服务器和应用程序在同一个进程里;

Threading Modal:线程模型,线程模型的具体做含意在之前我们已经详细的介绍过,此处将不一再阐述,但是需要知道的是,并非在此设置了某一种线程模型,那么这个服务器就是一个什么样的线程模型,而是在编程过程中来合理的安排。推荐默认的线程模型:Apartment;

点“OK”按钮之后,并将文件保存成AccUnt,会出现入下代码:

unit AccUnt;



{$WARN SYMBOL_PLATFORM OFF}



interface



uses

Windows, ActiveX, Classes, ComObj;



type

TAccemp = class(TComObject)

protected

end;



const

Class_Accemp: TGUID = @#{561FA61C-A985-4F70-8B5A-DF40BA9A7ED8}@#;

implementation



uses ComServ;



initialization

TComObjectFactory.Create(ComServer, TAccemp, Class_Accemp,

@#Accemp@#, @#实例分析@#, ciMultiInstance, tmApartment);

end.

我们对以上的代码作一个简短的分析:

Taccemp 是一个产生的类。回忆一下,在向导中我们指定Accemp为类名,Delphi自动的加入“T”。

Class_Accemp常量是一个代表COM服务器的GUID。服务器中实现的每个接口都会有自己的一个GUID;

单元的初始化部分包括一个单一却复杂的构造函数调用。这个调用建立了负责创建Taccemp COM对象的类厂。

TcomObjectFactory.Create在ComObj.Pas中定义如下:

constructor TComObjectFactory.Create(ComServer: TComServerObject;

ComClass: TComClass; const ClassID: TGUID; const ClassName,

Description: string; Instancing: TClassInstancing;

ThreadingModel: TThreadingModel);

begin

IsMultiThread := IsMultiThread or (ThreadingModel <> tmSingle);

if ThreadingModel in [tmFree, tmBoth] then

CoInitFlags := COINIT_MULTITHREADED else

if (ThreadingModel = tmApartment) and (CoInitFlags <> COINIT_MULTITHREADED) then

CoInitFlags := COINIT_APARTMENTTHREADED;

ComClassManager.AddObjectFactory(Self);

FComServer := ComServer;

FComClass := ComClass;

FClassID := ClassID;

FClassName := ClassName;

FDescription := Description;

FInstancing := Instancing;

FErrorIID := IUnknown;

FShowErrors := True;

FThreadingModel := ThreadingModel;

FRegister := -1;

end;

结合参数来理解这个过程。

在99%的情况下,用户简单地传递作为参数COMServer的全局ComServer对象。

第二个参数ComClass接受将由类厂创建的类。在此实例中,相要此类厂创建Taccemp实例。

第三个参数ClassID获得分配给Taccemp类的GUID。既Class_Accemp;

接下来,传递类名Accemp和类的描述Description。

Instancing参数仅应用于进程外COM服务器。在此可以忽略。

Delphi的COM对象向导填入了默认值ciMultiInstance。

最后一个参数接受一个表示线程模型的值,该模型由此对象支持。而以上所介始的各个参数都可以根据它的实现代码进行参考。其实,它的实现过程更多的就是将上边所列出的参数进行位置对应。这个过程留给读者朋友们分析。当然,即使对它的内部不是很了解,也无大碍。

由上边的描述,我们来分析一下:

TComObjectFactory.Create(ComServer, TAccemp, Class_Accemp,

@#Accemp@#, @#实例分析@#, ciMultiInstance, tmApartment);

创建一个COM对象Taccemp的实例Accemp,其GUID是Class_Accemp所代表的常量描述为’实例分析’,支持多线程。

COM对象离不开接口,只有通过接口才可以将COM对象提供的服务真零点切切的被客户端去调用,接下来,我们需要进行接口的代码填充,因为要实现登录和算法的简单实现过程,按照前边所提及的,如果两个联系不是很密切的功能我们尽量放在不同的接口中,立求接口的相对独立性,其实,实际中的情况也是如此。因为接口有不变性。负责登录的接口我们仅仅需要实现两个功能于同一个接口中,如下:

IAccIntf = Interface

[@#{ADDFAFFE-32D0-474F-909C-155E3906F0D1}@#]

Function GetLogn(UserName , UserPass : String) : Boolean;

Function GetSysTime : String;

end;

再此,笔者再次提醒,GUID应该在每个接口声明时都需要创建,假设此处将不进行接口的创建,在后边的接口分离中会出现“不支持接口”的错误提示信息,不必担心GUID会被用完,纵然再有几百年也用不完我们现有的GUID。不要想着Copy ?C Paster GUID;

{Delphi中,Shift + Ctrl + G 可以产生一个唯一的GUID}

实现算法的接口现在我们要吧只仅仅定义一个功能实现;这儿有一个问题,为了求得时间间隔,我们需不需要再次的再这个接口中添加一个求时间间隔过程呢?这就设计到系统分析的一些细节问题。如果是读者在自己写工程的时候,我建议,这个时间间隔写在客户端,即使是客户时间与服务器时间不相同也无妨,因为我们仅仅为的是取时间间隔,当然,如果是为了取得服务器的时间,也应该再在这个接口中填写一项功能实现。当然,我们可以调用其它的接口中的此功能,然而,为了一个很容易实现的功能进行接口分离或是转化,没有多大必要。本例是利用接口分离来获取其它接口中的取系统时间功能,此处是为了向读者朋友说明一些问题而这样设计的。此接口实现如下:

IAccSum = Interface

[@#{1D2F7597-6FCF-4706-810F-90A501953FFB}@#]

Function AccSum(AValue : Integer) : Integer;

end;

接口声明完成之后,需要写接口的实现过程。

在单元AccUnt中,可以看如下的代码:

……

Type

TAccemp = class(TComObject)

……

……

原文转自:http://www.ltesting.net