1. 程式人生 > >程序外元件的連線事件

程序外元件的連線事件

 COM中事件驅動技術探討





鄒飛
版本v1.0
2004年7月


目 錄
1. 問題的提出.... 3
2. 名次術語.... 3
3. 常用技術.... 3
3.1 緊密耦合事件(Tightly Coupled Events,TCE)... 3
3.1.1 連線點技術... 3
3.1.2 訊息佇列技術... 11
3.2 鬆散耦合事件(Loosely Coupled Events,LCE)... 11
3.2.1 COM+的事件驅動... 11
4. 總結.... 18



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(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寫也是完全一樣的),在裡面對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以上的機器執行。

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