COM中事件驅動技術探討

COM中事件驅動技術探討

 

 

 

 

 

鄒飛

版本v1.0

20047


 

1.     問題的提出.... 3

2.     名次術語.... 3

3.     常用技.... 3

3.1       緊密耦合事件(Tightly Coupled EventsTCE... 3

3.1.1       連接點技術... 3

3.1.2       消息隊列技... 11

3.2       鬆散耦合事件(Loosely Coupled EventsLCE... 11

3.2.1    COM+的事件驅動... 11

4.     .... 18

 


1.              問題的提出

類似於設計模式中Observer模式,在COM編程中,希望實現一種機制,使得對數據變化感興趣的若干部分能夠接受到數據的變化通知。一個典型的應用:計算機監控程序在計算機狀態數據發生變化時通知系統管理員、系統日誌程序、發送電子郵件等等。

2.              名次術語

訂閱者Subscriber:對數據感興趣的程序

發佈者Publisher:發佈數據變化通知的程序

激發事件Firing Event:發佈者發起的通知過程

源接口(Source/出(Outgoing)接口:發佈者和訂閱者之間達成的一致接口

接收器(Sink):訂閱者提供給發佈者的對象

3.              常用技術

3.1               緊密耦合事件(Tightly Coupled EventsTCE

3.1.1            連接點技術

COM中提供了連接點的技術用於實現事件驅動。

連接點技術的工作方式爲:

n         訂閱者通過查詢標準COM接口IConnectionPointContainer,詢問發佈者是否支持連接點機制

n         通過IConnectionPointContainerFindConnectionPoint方法得到某種特定類型的連接點,通過接口IConnectionPoint返回

n         訂閱者創建一個接收器(Sink)對象

n         訂閱者通過IConnectionPointAdvise方法把接收器對象加入到發佈者的接收者名單中,返回一個cookieDWORD的標識)

n         訂閱者通過IConnectionPointUnadvise取消對該事件的關注

從上面的工作方式可以發現,要實現連接點技術,只要訂閱者和發佈者遵循一定的接口規範,並對這些接口進行實現即可。

發佈者:實現IConnectionPointContainer的容器、支持IConnectionPoint的連接點

訂閱者:實現Sink對象

VC中,提供了多種機制對連接點的實現進行了簡化,使得開發連接點程序變得很簡單,在VC6VC7中均對連接點有很好的支持,同時又有一定差別,本文分別介紹。

實例介紹

首先介紹一下我們要實現的實例的功能描述:

發佈者:實現一個Add方法的接口,當Add方法被調用時,如果結果大於100,則調用OnAdd事件,將Add的參數傳出去,實現爲一個COMDLL

訂閱者:訂閱OnAdd事件,當OnAdd事件被調用時,輸出參數值,實現爲Dialog Based Application

Visual C++ 6.0

Visual C++5.0後提供了ATLActive Template Library,活動模板庫),它一套可用於開發輕量級COM組件的開發庫(在ATL裏使用非常複雜難懂的Template,理解起來很麻煩~~~

我們的第一種方法就是通過ATL來實現連接點:

發佈者:

n         通過ATL COM Wizard新建一個COM組件

項目名稱爲XAdd

n         通過ATL Object Wizard新建一個COM對象(Simple Object

Short Name設爲Add(其他的名字Wizard會自動產生),同時,在Attributes中鉤上Support Connection Points,以使得該COM對象支持連接點,這會自動產生一個_IAddEvents的接口。

n         Add增加Add方法

HRESULT Add([in] LONG a, [in] LONG b, [out] LONG* pVal);

n         _IAddEvents增加OnAdd方法

HRESULT OnAdd([in] LONG a, [in] LONG b);

n         F7編譯,自動註冊COM組件

n         CAdd的右鍵菜單中選擇Implement Connection Point,選擇_IAddEvents

Wizard會自動給CAdd增加上對IConnectionPointContainer接口的實現,並會增加CProxy_IAddEvents,它對IConnectionPoint接口進行了實現。

n         CAddAdd方法實現中增加對事件的觸發:

STDMETHODIMP CAdd::Add(LONG a, LONG b, LONG* pVal)

{

         *pVal = a + b;

         Fire_OnAdd(a, b);                // 觸發OnAdd事件

         return S_OK;

}

n         重新編譯、註冊即可。

 

訂閱者:

n         新建一個MFC AppWizard(exe),名爲XAddClient,類型爲Dialog based,其餘採用默認即可。

n         添加對ATL的支持(一種簡單的方法是選擇New ATL Class,由於不能在MFC Application中加入ATL Class,但這是ATL的支持已經被加入)

n         在項目中加入XAdd.h文件(發佈者的接口定義頭文件)

n         在項目中導入COM的類型庫

stdafx.h中加入:

#import "../XAdd.tlb" no_namespace, named_guids

n         新建一個類CEventSinkpublic繼承自IDispEventImpl<1, CEventSink, &DIID__IAddEvents, &LIBID_XADDLib>

namespace {

_ATL_FUNC_INFO OnAddInfo =

{

         CC_STDCALL,

         VT_EMPTY,

         2 ,

         {VT_I4, VT_I4}

};

}

 

class CEventSink : public IDispEventImpl<1, CEventSink, &DIID__IAddEvents, &LIBID_XADDLib>

{

public:

         CEventSink();

         virtual ~CEventSink();

 

         void __stdcall OnAdd(int a, int b);

 

         BEGIN_SINK_MAP(CEventSink)

         SINK_ENTRY_INFO(1, DIID__IAddEvents, 1, OnAdd, &OnAddInfo)

         END_SINK_MAP()

};

 

n         實現CEventSinkOnAdd方法

void __stdcall CEventSink::OnAdd(int a, int b)

{

         CString str;

         str.Format("%d %d", a, b);

         AfxMessageBox(str);

}

n         這樣,接收器對象就已經實現完成,下面只要把接收器AdviseXAdd對象上即可。

n         Dialog中加入Sink對象的Advise、並調用Add()方法,這個步驟比較簡單,可以參考例程

n         定義private變量:

         IAddPtr pAdd;

         DWORD dwCookie;

         CEventSink* pSink;

n         OnInitDialog()中創建COM對象

         CoInitialize(NULL);

         pAdd.CreateInstance(__uuidof(Add));

n         DestroyWindow中銷燬COM對象

       pAdd = NULL;

       CoUninitialize();

n         增加3個按鈕方法:OnCallAddOnAdviseOnUnadvise

void CXAddClientDlg::OnCallAdd()

{

                  LONG ret;

                  pAdd->Add(1, 2, &ret);

}

void CXAddClientDlg::OnAdvise()

{

                  if (pSink == NULL)

                  {

                            pSink = new CEventSink();

                            AtlAdvise(pAdd, (IUnknown*)pSink, DIID__IAddEvents, &dwCookie);

                  }

}

void CXAddClientDlg::OnUnadvise()

{

                  if (pSink != NULL)

                  {

                            AtlUnadvise(pAdd, DIID__IAddEvents, dwCookie);

                            delete pSink;

                  }

}

我們可以發現,ATL爲我們定義了一組WizardMacroTemplate等,使得實現COM對象變得很簡單,我們不需要去關心AddRef()Release()等很多通用方法的實現,ATL已經幫我們做得很好。

 

除了使用ATL作爲開發COM的開發庫,MFC也提供了很好的COM支持,因此也可以通過MFC實現連接點技術,本文不再介紹。

 

Visual C++ 7.0

Microsoft推出Visual Studio.NET後,同時也對VC進行了升級,在Visual C++ 7.0中,提供了很多對C++的擴充,包括更好的對COM的支持。

VC 7.0種可以通過__hook__unhook關鍵字更方便地實現事件驅動。

這裏給出一個具體的實現例子(開發環境爲:Microsoft Visual C++.NET 中文版):

發佈者:

n         VC 7.0中新建一個ATL項目,名爲XAdd

n         添加ATL 簡單對象,名爲Add,在選項中增加對連接點的支持:

n         這裏有個小小的問題,通過Wizard自動產生的接口定義是和接口的實現類定義放在一起的(Add.h),但接口定義應該是客戶可見的,而接口的實現類則不應該客戶可見,因此我們做個改動,把Add.h中的接口定義copy到另一個文件IAdd.h中,並在Add.h#include “IAdd.h”

n         IAdd中添加方法Add

HRESULT Add([in] LONG a, [in] LONG b, [out,retval] LONG* pVal);

n         _IAddEvents中添加事件方法OnAdd

HRESULT OnAdd([in] LONG a, [in] LONG b);

n         實現CAdd中的Add方法

STDMETHODIMP CAdd::Add(LONG a, LONG b, LONG* pVal)

{

     *pVal = a + b;

     __raise OnAdd(a, b);   // 觸發事件

     return S_OK;

}

n         編譯、註冊

 

訂閱者:

n         新建Win32 控制檯項目,名爲AddClient,應用程序設置中添加ATL支持

n         添加ATL支持

stdafx.h中增加:

#define _ATL_ATTRIBUTES

#include

#include

#include

n         添加類AddProxy,用於實現Sink對象,同時作爲客戶端事件驅動代理

頭文件AddProxy.h

#pragma once

 

#include

#include "../IAdd.h"

 

[ module(name = "Receiver") ];

[ event_receiver(com) ]

class CAddProxy

{

public:

     CAddProxy(void);

     virtual ~CAddProxy(void);

 

     void OnAdd(LONG a, LONG b);

private:

     void Hook(IAdd* pS);

     void UnHook(IAdd* pS);

     LONG Add(LONG a, LONG b);   // COM對象的包裝

     _COM_SMARTPTR_TYPEDEF(IAdd, __uuidof(IAdd));

     IAddPtr m_pAdd;

};

 

實現文件AddProxy.cpp

#include "StdAfx.h"

#include "./addproxy.h"

#include

 

using namespace std;

 

CAddProxy::CAddProxy(void)

{

     m_pAdd.CreateInstance("XAdd.Add");   // 創建COM對象

     Hook(m_pAdd);                        // 掛接事件

}

 

CAddProxy::~CAddProxy(void)

{

     UnHook(m_pAdd);                           // 取消掛接

     m_pAdd = NULL;                            // 銷燬COM對象

}

 

void CAddProxy::OnAdd(LONG a, LONG b)

{

     cout << a << b << endl;              // 事件代碼

}

 

void CAddProxy::Hook(IAdd* pS)

{

     __hook(&_IAddEvents::OnAdd, pS, &CAddProxy::OnAdd); // 掛接

}

 

void CAddProxy::UnHook(IAdd* pS)

{

     __unhook(pS);                        // 取消掛接

}

 

LONG CAddProxy::Add(LONG a, LONG b)

{

     LONG ret;

     m_pAdd->Add(a, b, &ret);

     return ret;

}

n         main()函數中創建Proxy對象,執行Add()方法。

int _tmain(int argc, _TCHAR* argv[])

{

     CAddProxy add;

     add.Add(1, 2);

     return 0;

}

n         編譯,執行。

如果我們看一下Wizard幫我們生成的代碼,我們可以發現,在VC 7.0裏對COM的支持多出了很多關鍵字,如__event__interface__raise__hook__unhook等,通過這些關鍵字要實現COM事件驅動是很簡單的(即使完全不採用Wizard,而手工編碼,也不復雜)

此外,需要說明的是:雖然VC 7.0是在VS.net中提供的,但採用這種方法寫出來的COM組件和接收器都可以在Windows 2000等沒有.Net Framework的機器上運行,即不需要.NET Framework的支持。

3.1.2            消息隊列技術

通過Microsoft提供的MSMQMicrosoft Message Queue Server,微軟消息隊列服務器)也可以實現緊密耦合的事件驅動,這不是本文的重點,這裏不敘述。

說明:MSMQ只在Windows 2000以後的操作系統提供,且不作爲系統的必選安裝,需要在安裝系統後再增加。

3.2               鬆散耦合事件(Loosely Coupled EventsLCE

3.2.1            COM+的事件驅動

雖然通過連接點技術可以實現COM中的事件驅動,但它存在着一些缺點:

n         發佈者和訂閱者生命週期緊密相關,對於企業應用不很合適

n         連接點在建立和斷開連接時需要多次交互,效率較低,對於分佈式應用環境存在問題

n         TCE沒有事件過濾的機制

針對這些問題,COM+中引入了一種發佈和訂閱LCE事件的機制,稱爲COM+事件(COM+ Event)。它有着很多好的特性,本文無法對COM+作更詳細的介紹,這裏只是結合一個實例說明COM+ Event的實現,而對於事件過濾、安全設定等高級選項請參考COM+相關書籍。

實例說明:設計一個股票價格發佈和訂閱系統,當股票價格發生變動時,發佈者自動通知訂閱者(調用訂閱者的方法)

實現步驟:

1、編寫事件組件

n         通過ATL COM Wizard創建COM組件工程XEvent

n         通過ATL Object Wizard創建組件StockEvent,無須設置Attributes中的Support Connection Points

n         IStockEvent增加方法NewQuote

HRESULT NewQuote([in] BSTR bsSymbol, [in] double dValue);

無須爲該方法實現,只須返回S_OK即可。

n         編譯,註冊

2、安裝事件組件(以Windows 2000 Professional爲例)

n         打開“控制面板”==〉“管理工具”下的組件服務

n         COM+應用程序上點右鍵,“新建”==〉“應用程序”,爲事件組件新建一個Application,名爲StockApp

n         在新建出的StockApp應用程序下的組件菜單中,點右鍵,“新建”==〉“組件”

n         選擇“安裝新的事件類”

n         將第一步編寫的事件組件導入,嚮導會自動發現組件以及組件中的接口。

n         安裝完成

3、編寫事件訂閱服務

n         通過ATL COM AppWizard新建一個COM組件項目,名爲StockSubscriber

n         導入事件組件接口

stdafx.h中加入

#import "../Test.tlb" raw_interfaces_only no_namespace, named_guids

n         在項目中加入文件XEvent.h

n         通過ATL Object Wizard新建一個Simple Object,名爲StockEventSubscriber,這一步工作只是爲了讓ATL自動幫我們產生idl文件以及coclassC++類包裝,所以接口IStockEventSubscriber對我們是沒有什麼用途的,可以把它刪掉(下面會介紹怎麼刪除),當然也可以不通過ATL Object Wizard而是自己寫idlCStockEventSubscriber

n         StockEventSubscriber.idl中修改coclass,使它的默認接口爲IStockEvent

修改library部分:

library STOCKSUBSCRIBERLib

{

       importlib("stdole32.tlb");

       importlib("stdole2.tlb");

       importlib("../XEvent/XEvent.tlb");

 

       [

              uuid(E04C02F3-F8B4-489C-B91F-A04D3DB5AEFD),

              helpstring("StockEventSubscriber Class")

       ]

       coclass StockEventSubscriber

       {

              [default] interface IStockEvent;

              interface IStockEventSubscriber;

       };

};

n         CStockEventSubscriber增加IStockEvent的實現(即當觸發事件會執行的代碼)

頭文件StockEventSubscriber.h

class ATL_NO_VTABLE CStockEventSubscriber :

         public CComObjectRootEx<CComSingleThreadModel>,

         public CComCoClass<CStockEventSubscriber, &CLSID_StockEventSubscriber>,

         public IDispatchImpl<IStockEventSubscriber, &IID_IStockEventSubscriber, &LIBID_STOCKSUBSCRIBERLib>,

         public IDispatchImpl<IStockEvent, &IID_IStockEvent, &LIBID_XEVENTLib>

BEGIN_COM_MAP(CStockEventSubscriber)

         COM_INTERFACE_ENTRY(IStockEventSubscriber)

         COM_INTERFACE_ENTRY(IStockEvent)

         COM_INTERFACE_ENTRY2(IDispatch, IStockEvent)

END_COM_MAP()

 

// IStockEvent

public:

         STDMETHOD(NewQuote) (BSTR bsSymbol, double dValue);

實現文件StockEventSubscriber.cpp

STDMETHODIMP CStockEventSubscriber::NewQuote(BSTR bsSymbol, double dValue)

{

         TCHAR buf[100];

         _stprintf(buf, _T("%s %lf"), bsSymbol, dValue);

         ::MessageBox(NULL, buf, _T("Stock Price"), MB_OK);

         return S_OK;

}

n         編譯,註冊

n         附:上面的步驟其實產生了一個沒有任何用途的“空”接口IStockEventSubscriber,下面給出一個步驟去除它

n         StockEventSubscriber.idl文件中,刪除接口定義

       [

              object,

              uuid(1FBF1C53-35B5-4E59-B821-AE68D16E4536),

              dual,

              helpstring("IStockEventSubscriber Interface"),

              pointer_default(unique)

       ]

       interface IStockEventSubscriber : IDispatch

       {

       };

       刪除coclass中的接口說明

       interface IStockEventSubscriber;

n         StockEventSubscriber.h頭文件中,刪除對IstockEventSubscriber接口的實現

public IDispatchImpl<IStockEventSubscriber, &IID_IStockEventSubscriber, &LIBID_STOCKSUBSCRIBERLib>,

         COM_INTERFACE_ENTRY(IStockEventSubscriber)

修改COM_INTERFACE_ENTRY2(IDispatch, IStockEvent)COM_INTERFACE_ENTRY(IDispatch)

n         編譯,註冊

4、安裝事件訂閱服務

n         在“組件服務”==〉“StockApp”的組件下新建組件,選擇“安裝新組件”,安裝StockSubscriber.dll

5、訂閱服務

n         在“組件服務”==〉“StockApp”的組件==〉“StockSubscriber.StockEventSubscriber.1==〉“訂閱”點右鍵,“新建”==〉“訂閱”

n         選擇訂閱方法,直接選中IstockEvent,點“下一步”

n         選擇事件類,選中某個事件類,點“下一步”

n         訂閱選項,爲訂閱起一個名稱StockSubscriber,選中“立即啓用該訂閱”,點“下一步”

n         完成訂閱

5、測試事件驅動

我們用VB寫一個簡單的COM組件調用(用VC寫也是完全一樣的),在裏面對XeventNewStock()方法進行調用,會發現StockSubscriber中的NewStock方法也被調用了(彈出對話框),證明事件被正確的訂閱了。

代碼如下:

Private Sub Command1_Click()

    Set StockPriceEvent = CreateObject("XEvent.StockEvent")

    StockPriceEvent.NewQuote "Test", 100

End Sub

6、製作COM+安裝文件

COM+組件服務自動提供了對組件打包分發的功能,可以在“組件服務”==〉“StockApp”點右鍵,“導出”

然後可以自動生成安裝文件,以後可以直接在其他機器上安裝,COM+組件就可以正確安裝。

 

其他說明:

n         採用COM+實現還可以獲得COM+的其他很多特性,比如JIT、對象池、安全特性等。

n         COM+至少要在Windows 2000以上的機器才能夠使用。

 

4.              總結

實現COM的事件驅動包括TCELCE兩種模式,TCE可以通過Connection Point或消息隊列實現,LCE通過COM+訂閱者模型實現。

實現連接點:VC7實現起來更簡單,對ATL的支持更全面、更穩定,而VC6.0ATL則經常容易出現一些小問題。

COM+訂閱者模型:COM+可以提供更好的運行特性,以及更靈活的配置管理,但只能在Windows 2000以上的機器運行。

 

在實現事件驅動時根據具體情況選擇一種實現。

 

 

下載PDF版本

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章