COM中事件驅動技術探討
鄒飛
版本v1.0
2004年7月
目 錄
3.1 緊密耦合事件(Tightly Coupled Events,TCE)... 3
3.2 鬆散耦合事件(Loosely Coupled Events,LCE)... 11
1. 問題的提出
類似於設計模式中Observer模式,在COM編程中,希望實現一種機制,使得對數據變化感興趣的若干部分能夠接受到數據的變化通知。一個典型的應用:計算機監控程序在計算機狀態數據發生變化時通知系統管理員、系統日誌程序、發送電子郵件等等。
2. 名次術語
訂閱者Subscriber:對數據感興趣的程序
發佈者Publisher:發佈數據變化通知的程序
激發事件Firing Event:發佈者發起的通知過程
源接口(Source)/出(Outgoing)接口:發佈者和訂閱者之間達成的一致接口
接收器(Sink):訂閱者提供給發佈者的對象
3. 常用技術
3.1 緊密耦合事件(Tightly Coupled Events,TCE)
3.1.1 連接點技術
COM中提供了連接點的技術用於實現事件驅動。
連接點技術的工作方式爲:
n 訂閱者通過查詢標準COM接口IConnectionPointContainer,詢問發佈者是否支持連接點機制
n 通過IConnectionPointContainer的FindConnectionPoint方法得到某種特定類型的連接點,通過接口IConnectionPoint返回
n 訂閱者創建一個接收器(Sink)對象
n 訂閱者通過IConnectionPoint的Advise方法把接收器對象加入到發佈者的接收者名單中,返回一個cookie(DWORD的標識)
n 訂閱者通過IConnectionPoint的Unadvise取消對該事件的關注
從上面的工作方式可以發現,要實現連接點技術,只要訂閱者和發佈者遵循一定的接口規範,並對這些接口進行實現即可。
發佈者:實現IConnectionPointContainer的容器、支持IConnectionPoint的連接點
訂閱者:實現Sink對象
在VC中,提供了多種機制對連接點的實現進行了簡化,使得開發連接點程序變得很簡單,在VC6和VC7中均對連接點有很好的支持,同時又有一定差別,本文分別介紹。
實例介紹
首先介紹一下我們要實現的實例的功能描述:
發佈者:實現一個Add方法的接口,當Add方法被調用時,如果結果大於100,則調用OnAdd事件,將Add的參數傳出去,實現爲一個COM的DLL
訂閱者:訂閱OnAdd事件,當OnAdd事件被調用時,輸出參數值,實現爲Dialog Based Application。
Visual C++ 6.0
Visual C++從5.0後提供了ATL(Active 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 在CAdd的Add方法實現中增加對事件的觸發:
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 新建一個類CEventSink,public繼承自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 實現CEventSink的OnAdd方法
void __stdcall CEventSink::OnAdd(int a, int b)
{
CString str;
str.Format("%d %d", a, b);
AfxMessageBox(str);
}
n 這樣,接收器對象就已經實現完成,下面只要把接收器Advise到XAdd對象上即可。
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個按鈕方法:OnCallAdd、OnAdvise、OnUnadvise
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爲我們定義了一組Wizard、Macro、Template等,使得實現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提供的MSMQ(Microsoft Message Queue Server,微軟消息隊列服務器)也可以實現緊密耦合的事件驅動,這不是本文的重點,這裏不敘述。
說明:MSMQ只在Windows 2000以後的操作系統提供,且不作爲系統的必選安裝,需要在安裝系統後再增加。
3.2 鬆散耦合事件(Loosely Coupled Events,LCE)
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文件以及coclass的C++類包裝,所以接口IStockEventSubscriber對我們是沒有什麼用途的,可以把它刪掉(下面會介紹怎麼刪除),當然也可以不通過ATL Object Wizard而是自己寫idl和CStockEventSubscriber。
n 在StockEventSubscriber.idl中修改coclass,使它的默認接口爲IStockEvent
修改library部分:
library STOCKSUBSCRIBERLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
importlib("../XEvent/XEvent.tlb");
[
uuid(E
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(1FBF
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
n 選擇訂閱方法,直接選中IstockEvent,點“下一步”
n 選擇事件類,選中某個事件類,點“下一步”
n 訂閱選項,爲訂閱起一個名稱StockSubscriber,選中“立即啓用該訂閱”,點“下一步”
n 完成訂閱
5、測試事件驅動
我們用VB寫一個簡單的COM組件調用(用VC寫也是完全一樣的),在裏面對Xevent的NewStock()方法進行調用,會發現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的事件驅動包括TCE和LCE兩種模式,TCE可以通過Connection Point或消息隊列實現,LCE通過COM+訂閱者模型實現。
實現連接點:VC7實現起來更簡單,對ATL的支持更全面、更穩定,而VC6.0的ATL則經常容易出現一些小問題。
COM+訂閱者模型:COM+可以提供更好的運行特性,以及更靈活的配置管理,但只能在Windows 2000以上的機器運行。
在實現事件驅動時根據具體情況選擇一種實現。