元件技術的本質COM例項分析一 (轉)

worldblog發表於2007-08-14
元件技術的本質COM例項分析一 (轉)[@more@]

COM:namespace prefix = o ns = "urn:schemas--com::office" />

程式內COM(In-Process Server)

程式內的COM伺服器(In-Process Com Server)在這前已經簡單的介紹過,在此,只對一些需要明白、理解的知識點進行闡述,再進行程式設計可能會更好一點;程式內伺服器是由於它們在DLL內實現而獲得這個名稱的。因此,伺服器佔擗了和使用它的應用一樣的地址空間(程式)。所有的程式內COM伺服器輸出四個標準:DllRegisterServer、DllUnregisterServer、DllGetClass和DllCanUnloadNow。根據字面意思,我們也已經可以感覺的出這些函式的工作都會是什麼。當然,已經為我們提供了這些函式的預設實現。因此,讀者不必自己寫程式碼來實現這些函式,但應該理解他們具體是做什麼的。

²  DllRegisterServer。DllRegisterServer以兩種方式自動。的Register Server選單選項呼叫它,的命令列應用程式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物件是集合的一部分時才使用它。

DwClntext: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按Enter鍵,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_PLATFOOFF}

 

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 – 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)

  ……

……

實現類TAccEmp並沒有對介面實現,此處我們只需要將剛剛宣告的兩個介面讓其實現就可以了。伺服器端程式碼如下:

 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-956690/,如需轉載,請註明出處,否則將追究法律責任。

相關文章