組件技術的本質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??梢愿杏X的出來,這個函數和DllRegisterServer是作相反的工作,實際上它們就是一個相逆的過程,它移走了DllRegisterServer放在Windows注冊表中的所有條目??梢杂肐DE里的 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指針??嗨埱蟮腉UID在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.kjueaiud.com