您的位置:首页 > 其它

深入解析连接点

2008-04-14 19:54 162 查看
Connection Point Internals Working with ATL 8

示例代码

关键字:

COM ATL 连接点 进程间通信 代理存根 双接口 自定义接口

0-我要讲什么

(1) 介绍连接点
(2) 实现一个简单的连接点
(3) 用双接口实现连接点来进行进程间通信
(4) 用自定义接口实现连接点来进行进程间通信
(5) 小结

1-介绍连接点

什么是连接点?描述一个概念不是我的长项,所以我就从《深入解析ATL》(Brent Rector、Chris Shells著 潘爱民 新语 译)上摘抄了三段来充数。 J
术语连接点(connection point)指的是一种逻辑上的反馈机制, 这种反馈机制允许对象暴露其“调用一个或者多个指定接口”的能力。
一个连接有两个部分:对指定接口的方法产生调用的对象:被称作源(source)或者连接点(connection point);以及实现该接口(即接收调用的接口)的对象,被称作接收对象(sink object) ,有些书上也叫接收器 。
虽然这个连接点协议在套间内部使用是可以被接受的,但是对于跨越套间边界的使用是非常低效的(当考虑到来回调用开销的时候)。

术语一类的东西向来让我找不着北,要对概念理解的更深入,就必需多实践,在实践中发现问题, 解决问题。吃一堑,长一智嘛 J

2-实现一个简单的连接点

在实现连接点之前,相信还是十分有必要简单的介绍一下回调接口的概念和方法。回调接口的原理和使用很简单,和回调函数差不多,唯一不同的是回调函数只有一个,而作为函数集合的接口,回调接口其实就是回调函数的集合。大家如果不明白回调接口的话,推荐大家去看看杨老师的文章,明白之后再回过头来看连接点 J 。(谁是杨老师?什么!你连杨老师都不知道?这,这,这 … 赶紧去搜索一下 J )

好了,第一个例子我想让代码尽量的少,以方便我们看清连接点的本质,所以我要用自定义接口来实现。
我要实现的例子很简单,
有个组件叫Teacher实现了ISpeaker接口,并实现一个连接点_ISpeakerEvents
另一个组件叫Student,实现了_ISpeakerEvents这个连接点的接收器。
终于可以动手写程序了--------- J J J
(1)首先新建一个工程,解决方案叫SimpleSpeaker,工程名称Teacher



图2-1
(2)选择工程属性,直接用默认的配置,直接点“完成” J
(关于各选项的作用我这里就不介绍了,相信大家应该都在介绍COM的书上或多或少的了解过)



图2-2
(3)我们将看到下图的一个程序类图。紧接着,在类视图中添加一个类。



图2-3
(4)插入一个ATL简单对象,简称:Speaker,并“下一步”,在选项中我们用的是自定义接口,并勾上了连接点,选好之后点“完成”。 J



图2-4



图2-5
(5)ATL默认的连接点接口定义是一个双接口,而我想要的是一个自定义接口的连接点,所以需要在IDL文件中做下修改:


library TeacherLib




...{




// 此处省略N行 … …




interface _ISpeakerEvents : IUnknown // 由 dispinterface _ISpeakerEvents 修改而来






...{




// 为了又双接口改成自定义接口所做的修改三个地方而以




// +_+ 改完记得编译一下这个文件




//properties:




//methods:




[ helpstring ( " 方法 OnHearSpeak" )] HRESULT OnHearSpeak ([ in ] BSTR bstrSpeech );




};




// 此处省略N行 … …




coclass Speaker






...{




[ default ] interface ISpeaker ;




[ default , source ] interface _ISpeakerEvents ; // dispinterface _ISpeakerEvents;




};




};

修改好后最后在解决方案管理器里面先单独编译一下.idl文件,这是个好的习惯。
(6)好了,我们可以添加方法了,分别给ISpeaker和_ISpeakerEvents添加一个方法,老师有了一个Speak方法(说话嘛),学生有了一个OnHearSpeak(听到老师讲)。
添加后的IDL如下:


// 此处省略N行 … …




interface ISpeaker : IUnknown




...{


[ helpstring ( " 方法 Speak" )] HRESULT Speak ([ in ] BSTR bstrSpeech );


};




// 此处省略N行 … …




interface _ISpeakerEvents : IUnknown // 由 dispinterface _ISpeakerEvents 修改而来




...{


// 为了又双接口改成自定义接口所做的修改三个地方而以


// +_+ 改完记得编译一下这个文件


[ helpstring ( " 方法 OnHearSpeak" )] HRESULT OnHearSpeak ([ in ] BSTR bstrSpeech );


};

这里要注意,如果你上一步的IDL修改正确,给_ISpeakerEvents添加的接口是不会有id属性的,如果你发现你的OnHearSpeak前面有id什么的,那说明你上一步做漏了什么地方。
顺序的话,我们就可以开始用向导帮我们实现连接点了。
(7)在类视图中右键选中CSpeaker类->添加->添加连接点



图2-6
把源接口选到里的_ISpeakerEvents选到“实现连接点”后点“完成”,我们就可以从类视图中打开 CProxy_ISpeakerEvents <T>类,看看向导帮我们生成了什么代码。



图2-7
(7)我把对代码的分析写在代码里了,大家看看应该就明白了。


template < class T >




class CProxy_ISpeakerEvents :




public IConnectionPointImpl < T , & __uuidof ( _ISpeakerEvents )>






...{




public :




// 看看向导为我们做了什么,还隐藏了什么




HRESULT Fire_OnHearSpeak ( BSTR bstrSpeech )






...{




HRESULT hr = S_OK ;




T * pThis = static_cast < T *>( this );




// 注意这个 m_vec, 想知道他是干什么吗?你可以用“转到定义”跟到 ATL 源代码里




// 我们将发现他是一个 CComDynamicUnkArray ,从名字你应该猜出一二了吧




// 是一个 IUnknow 的 Array +_+ 再往下看你就更明白了




int cConnections = m_vec . GetSize ();






// 连接点和回调接口的一个不同就在这里。 IConnectionPointImpl 类为我们维护了




// 一个 m_vec ,用来保存多个接收器的连接,就相当于有多个回调接口,这里比回调




// 接口多做的事情就是需要遍历这个 vector 来给每个接收器都发去消息 :)




for ( int iConnection = 0; iConnection < cConnections ; iConnection ++)






...{




pThis -> Lock ();




CComPtr < IUnknown > punkConnection = m_vec . GetAt ( iConnection );




pThis -> Unlock ();






// 再看这里,这里用强制转换的方式把 IUnknown* 转换成对应的连接点接口类型




_ISpeakerEvents * pConnection = static_cast < _ISpeakerEvents *>( punkConnection . p );






if ( pConnection )






...{




// 看看,和回调接口的使用像不像,可能就到现在你觉得和回调接口唯一




// 不一样的就是不知道,接收器是怎么把“回调接口”指针传给连接点的。




// 呵呵,去看看 atlcom.h 文件中的




// STDMETHODIMP IConnectionPointImpl<T, piid, CDV>::Advise(IUnknown* pUnkSink,DWORD* pdwCookie)




// 相信看完你就会说,这不就是回调接口嘛 :)




hr = pConnection -> OnHearSpeak ( bstrSpeech );




}




}




return hr ;




}




};

(8)编译一下整个工程,成功,恭喜你连接器已经实现好了。
(9)我们来实现一个简单的客户端,一个控件台程序,够简单了吧。
在公共头文件那勾上ATL就实现起来就更简单了 J



图2-8
点“完成”后就可以添加代码了,添加一个C++类,头文件如下


// 大家注意了,下面这个文件是由 MIDL 编译生成的,里面包含有 Teacher 工程的




// 接口定义,当然你也可以使用 #import tlb 的方法来把接口定义包含进来




#include "../Teacher/Teacher.h"






/**//**




* @brief 这就是一个最简单的接收器




*/




class Listener : _ISpeakerEvents






...{




public :




Listener ( void );




~ Listener ( void );




public :




STDMETHOD ( QueryInterface )( const struct _GUID & iid , void ** ppv );




ULONG __stdcall AddRef ( void );




ULONG __stdcall Release ( void );




STDMETHOD ( OnHearSpeak )( BSTR bstrSpeech );




};

实现细节我就不多说,大家可以看代码。其实就是实现了一个类,一个继承了_ISpeakerEvents接口的类,因为是个接口的缘故,他需要实现一下QueryInterface、AddRef、Release三个方法。

用连接点来实现进程间通信

为什么Advise会失败?
第一个例子的Teacher组件是一个DLL,也就是一个进程内组件。那么如果我们在实现上面那个例子的第(2)步选择服务器类型时选的是EXE或者是服务,其它都不变,大家觉得会不会成功呢?如果你试过之后,应该会发现,在AtlAdvise的时候会出错,为什么呢?
这是我为什么想写这篇文章的初衷。我发现这个问题的时候,跟到ATL的源码,发现在atlcom.h中的
STDMETHODIMP IConnectionPointImpl < T , piid , CDV >:: Advise ( IUnknown * pUnkSink , DWORD * pdwCookie )
这个函数调用的时候,会去QueryInterface一下_ISpeakerEvents接口,就是检查一下接收器是否实现了连接点接口,函数返回失败――没找到这个接口的实现。很奇怪的是明明我们的Listener类继承了_ISpeakerEvents,这到底是为什么呢?原因出在跨越了进程,或者说是跨越了套间(呵,套间这东西和连接点一样,比较拗口,下次有空再解释 J )。
我们都知道在COM中,存在的叫做代理跟存根的东西,这两个东西是做什么的呢?在Windows系统中每个进程都有自己的地址空间,不同进程间的指针参数是不能直接传递的,因为在进程A中指定0x00400001的指针,到了进程B中所指的东西就不是原来你想要的了,所以需要用到列集(marshaling)。又是概念,简单的说列集就是对参数进行调整――使得进程A调用进程B的数据像是在自己的进程空间里一样。代理存根正是用来做列集(也就是调整)这个事情的,而标准的代理存根是依赖于IDL文件的,也就是说,如果你不想自己实现代理存根,那你就必需实现IDL,下在这句话很重要,请一定记住:
要传递的接口指针,必需用IDL定义,只有这样,标准的代理存根才能把接口指针所指的内容正确的调整到其它进程空间,否则你就自己实现代理跟存根。

拿到上面的例子中说明,就是Listener这个接口(实现了IUnknown的就是接口,而且我们也确实是把他当成了一个IUnknown来用的)必需用IDL定义之后,代理存根才能正确的调整参数,才不会advise失败,像上面那样只是简单的继承,接口指针传递到另一个进程空间的时候,他所表示的意义已经完全不同了,所以才会出现QueryInterface找不到_ISpeakerEvents的情况。

说的比较拗口,不知道大家明白没有,下面我就开始介绍第二个例子,用双接口实现连接点进行进程间通信:

3-用双接口实现连接点来进行进程间通信

我们将创建三个工程,Teacher.exe、Student.exe、Client.exe,好了,我们开始吧。 J
(1) 我们首先我实现Teacher,这是一个ATL 进程外服务器:
第一步,创建工程:



图 3-1
工程设置如下:



图 3-2
第二步,我们创建ISpeaker接口,添加一个“ATL简单对象”Speaker,接口设置如下,记得勾上连接点。



图 3-3
第三步,添加接口方法
点击完成之后,我们还必需对生成的idl进行修改。分别给ISpeaker和_ISpeakerEvents加个方法。
ISpeaker你可以用向导来生成,这样向导能帮你在Speaker.h和Speaker.cpp中生成对应的函数生明。_ISpeakerEvents的方法就得自己在idl里面手动添加了 J




interface ISpeaker : IDispatch ...{




[ id (1), helpstring ( " 方法 Speak" )] HRESULT Speak ([ in ] BSTR bstrSpeech );




};




dispinterface _ISpeakerEvents






...{




properties :




methods :




[ id (1), helpstring ( " 方法 OnTalk" )] HRESULT OnTalk ([ in ] BSTR bstrSpeech );




};

第四步,用向导生成连接点实现代码
修改好idl后,编译一下idl文件,确认idl编译成功后,我们右键连接点的实现类,选择“添加”-》添加连接点。



图 3-4



图 3-5
按照图 3-4和3-5,向导会帮我们生成好连接点实现类,在 _ISpeakerEvents_CP.h 文件中,并在Speaker.h中为我们添加连接点相关的代码,我这里就不细讲,大家自己看一下吧。
第五步,调用连接点的方法,Teacher工程完成。
用向导实现好连接点后,我们就要以使用连接点的方法了:


STDMETHODIMP CSpeaker :: Speak ( BSTR bstrSpeech )






...{




// TODO: 在此添加实现代码




Fire_OnTalk ( bstrSpeech );




return S_OK ;




}

当然这里的Fire_OnTalk只是一个空架子,真正的实现,也就是连接点的接收器,我们下面来实现。
(2)创建Student工程,这也是一个ATL进程外服务器。
第一步,创建工程,参考图 3-1和3-2。
第二步,创建IListener接口



用默认的选项,直接点击完成
第三步,实现接收器,这里需要我们手动添加代码:

接着给IListener接口添加两个方法,用来建立和断开连接点:
SpeakerEventsImpl :: DispEventAdvise 和 SpeakerEventsImpl :: DispEventUnadvise 。




interface IListener : IDispatch ...{




[ id (1), helpstring ( " 方法 ListenTo" )] HRESULT ListenTo ([ in ] IUnknown * pSpeaker );




[ id (2), helpstring ( " 方法 StopListening" )] HRESULT StopListening ( void );




};

实现上很简单,就是分别调用了


STDMETHODIMP CListener :: ListenTo ( IUnknown * pSpeaker )






...{




// TODO: 在此添加实现代码




HRESULT hr = StopListening ();




if ( FAILED ( hr ))






...{




return E_FAIL ;




}






hr = SpeakerEventsImpl :: DispEventAdvise ( pSpeaker , & __uuidof ( _ISpeakerEvents ));




if ( SUCCEEDED ( hr ))






...{




m_spSpeaker = pSpeaker ;




}




return S_OK ;




}






STDMETHODIMP CListener :: StopListening ( void )






...{




// TODO: 在此添加实现代码




HRESULT hr = S_OK ;




if ( m_spSpeaker )






...{




hr = SpeakerEventsImpl :: DispEventUnadvise ( m_spSpeaker , & __uuidof ( _ISpeakerEvents ));




// 这里要记得释放,不然你在 Stop 的时候 Student 还会在运行,因为引用计数 ...




m_spSpeaker . Release ();




}




return hr ;




}

这里要说的是,这两个函数大家如果跟代码进去的话就会发现,他们其实就是对API函数AtlAdvise和AtlUnadvise的一层包装。
好了,这样,我们的Student也实现了。
(2) 实现调用两个ATL服务器的客户端Client
第一步,创建一个控制台应用程序,加上“ATL”公共头文件



第二步,实现调用两个接口,为了让代码看上去少一点,这里我把返回值判断给去掉了,大家可不要这样啊 J
最后一步,编译、运行Client工程,成功 J


CComPtr < ISpeaker > spSpeaker ;




CComPtr < IListener > spListener ;




DWORD dwCookie = 0;








HRESULT hr = S_OK ;




hr = spSpeaker . CoCreateInstance ( __uuidof ( Speaker ));




if ( SUCCEEDED ( hr ))






...{




spListener . CoCreateInstance ( __uuidof ( Listener ));




if ( SUCCEEDED ( hr ))






...{




hr = spListener -> ListenTo ( spSpeaker );




hr = spSpeaker -> Speak ( CComBSTR ( _T ( " 同一个世界,同一个梦想。 " )));




hr = spListener -> StopListening ();




spListener . Release ();




}




spSpeaker . Release ();




}

在这个例子中,我们的连接点是实现在Teacher.exe中的,接收器是实现在Student.exe中的,两个进程用连接点正确的进行了通信,那如果我们想用自定义接口来实现进程间通信吗?继续往下看 J

4-用自定义接口实现连接点

我们终于要来解决“2-实现一个简单的连接点””一节最后留下的问题了。
连接点进行进程间通信的关键在于 代理/存根,只要进程间的通信是通过 代理和存根的,COM就会帮我们隐藏进程间的通信,也就是说进程间通信对两个COM组件来说是透明的。好了,费话少说,我们开始。
(1)Teacher
Teacher的实现我们可以参照 “2-实现一个简单的连接点”,唯一的不同在于上面是一个DLL,这次我们要让他是一个进程外组件,是个EXE。大家在创建工程时的工程设置里注意一下就可以了,其它步骤一模一样。

(2)Student
第一步,创建一个进程外组件,和上面一样。
第二步,添加IListener接口,和上面一样,我们用向导添加一个“ATL简单对象”。
第三步,这回不一样了,我们要做的是去teacher.idl中把_ISpeakerEvents的接口定义复制到Student.idl中。


[




uuid (F96925D0-986D-4727-93C5-C1699BA72CA4),




helpstring ( "_ISpeakerEvents 接口 " )




]




interface _ISpeakerEvents : IUnknown // 由 dispinterface _ISpeakerEvents 修改而来






...{




// 为了又双接口改成自定义接口所做的修改三个地方而以




// +_+ 改完记得编译一下这个文件




//properties:




//methods:




[ helpstring ( " 方法 OnHearSpeak" )] HRESULT OnHearSpeak ([ in ] BSTR bstrSpeech );






};



然后在idl的最后找到coclass Listener,让CListener类也实现_ISpeakerEvents接口。


coclass Listener






...{




[ default ] interface IListener ;




interface _ISpeakerEvents ;




};



第四步,接着我们当然是要在CListener类中实现_ISpeakerEvents接口
在Listener.h文件中添加下面三行后,在cpp文件中实现OnHearSpeak


public _ISpeakerEvents




COM_INTERFACE_ENTRY ( _ISpeakerEvents )




STDMETHOD ( OnHearSpeak )( BSTR bstrSpeech );

第五步,实现创建和断开连接点的两个方法,方法同上。
第六步,哈哈,这里可以注意了,这里是用自定义接口实现连接点的一个小技巧,是我从微软的例子那学来的,不过稍微有点不同。
去Teacher.idl中把_ISpeakerEvent相关的定义注释掉

_ISpeakerEvents_CP.h 就编译不过了,所以,你要在 _ISpeakerEvents_CP.h 文件头上加上对 Student .h的包含,哈哈,是不是很假呢?


library TeacherLib






...{




importlib ( "stdole2.tlb" );




// [




// uuid(F96925D0-986D-4727-93C5-C1699BA72CA4),




// helpstring("_ISpeakerEvents 接口 ")




// ]




// interface _ISpeakerEvents : IUnknown // 由 dispinterface _ISpeakerEvents 修改而来




// {




// // 为了又双接口改成自定义接口所做的修改三个地方而以




// // +_+ 改完记得编译一下这个文件




// //properties:




// //methods:




// [helpstring(" 方法 OnHearSpeak")] HRESULT OnHearSpeak([in] BSTR bstrSpeech);




//




// };




[




uuid (7AEF586B-9EEB-4554-90E0-41583892086C),




helpstring ( "Speaker Class" )




]




coclass Speaker






...{




[ default ] interface ISpeaker ;




// [default, source] interface _ISpeakerEvents; // dispinterface _ISpeakerEvents;




};




这样一来你的连接点实现文件

Client


// 哈哈,微软的做法是另外再定义一个头文件,专门来放接口的定义,




// 但这样有个不方便的地方就是你接口改变的时候那个头文件也要跟




// 着变,虽然一般不会变,但是如果接口变了,而头文件中的定义忘记




// 修改,那样的错误可能要让你找上一阵子了。 :)




#include "../Student/Student.h"






template < class T >




class CProxy_ISpeakerEvents :




public IConnectionPointImpl < T , & __uuidof ( _ISpeakerEvents )>






...{




… …


(3)

和用双接口实现时一模一样,我就不细讲了,大家看代码吧。

小结

多看看ATL源码对我们很有帮助。

P s:源代码编译说明,我是用vs2005 sp1实现的。因为工程之间有相互依赖性,我比较懒,没设工程依赖,如果大家在编译时提示错误说:发现少文件或lib,那肯定是你少编译了某个工程,如ps工程什么的。
Gook luck J
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: