DCOM 揭秘:DCOM 教程,第 7 步






4.92/5 (38投票s)
终于!我们通过使用 MFC、AppWizard 和 ClassWizard 编写客户端来结束我们的教程,就像在那些美好的日子里一样(叹息……)
图 1。HelloCli
示例程序的图像。
引言
欢迎来到我们的 DCOM 教程的第 7 步。这是最后一步!
如果你想跟着本教程学习,并在此过程中添加代码和使用 Visual C++ 向导,那就太好了。事实上,我非常、非常强烈地推荐这样做,否则本教程将是电子墨水(?)的大量浪费。但是,我在写教程的过程中,也是严格按照教程进行的,并且就像我告诉你们应该做的那样,开发代码并使用 Visual C++ 向导。事实上,截图就是我为每个步骤开发文件时的截图!要下载这些已经开发好的代码以与你自己的代码进行比较,只需点击每个步骤顶部的“下载第 n 步文件 - n KB”链接。在教程的问题和解答页面,还有一个所有步骤文件的存档。我仍然建议你跟着我们一起学习;这样,你就可以在编码的同时学习。如果你在学习过程中遇到任何问题,请随时
- 发送电子邮件给我:brian@harttechservices.com。
- 在此页面底部的留言板上发帖。
- 查看本教程的问题和解答页面。
请记住,本教程中我们开发软件的步骤如下:
- 第 1 步:使用 ATL COM AppWizard 创建服务器
HelloServ
。 - 第 2 步:修改 AppWizard 提供的启动文件。
- 第 3 步:使用 New ATL Object Wizard 向服务器添加一个简单的 COM 对象,即
HelloWorld
对象。 - 第 4 步:修改
IHelloWorld
接口以包含SayHello()
方法。 - 第 5 步:向连接点源接口
DHelloWorldEvents
添加一个事件方法OnSayHello()
。 - 第 6 步:构建服务器,并在服务器计算机上安装它。
- 第 7 步:创建一个 MFC 客户端
HelloCli
,该客户端调用服务器并处理连接点事件接收器。
我们目前正在进行本教程的第 7 步(最后一步!),我们将组装一个名为 HelloCli
的小型 MFC 程序来测试我们的服务器。让我们开始吧!
第 7 步:创建 MFC HelloCli
客户端以测试服务器
从上面的截图可以看到,我使用 MFC AppWizard (EXE) 构建了一个基于对话框的应用程序。我添加了一个状态窗口来报告状态,这样我就可以看到错误发生在何处,并且我还成功地处理了连接点。为了向状态窗口(它只是一个设置为只读样式的 Edit 控件)添加文本,我使用 ClassWizard 为对话框类添加了一个 CString
成员变量 m_strStatus
,然后每当我需要向编辑控件添加消息行时,这段代码都可以轻松完成
m_strStatus += "This is a status line for the edit control\r\n"; UpdateData(FALSE);列表 1。向编辑控件添加状态行。
我们使用 CString
的 +=
运算符将文本添加到 m_strStatus
的内容中,然后调用 UpdateData(FALSE)
将成员变量的内容从变量移动到编辑控件。
为了开始我的示例项目,我打开了 New 对话框,点击了 'MFC AppWizard (EXE)',然后为我的项目输入了 'HelloCli
' 作为名称。完成 AppWizard 后,我打开了 STDAFX.H
文件,并添加了 列表 2 中加粗显示的行
#if !defined(AFX_STDAFX_H__8495B5E0_67FF_11D4_A358_00104B732442__INCLUDED_) #define AFX_STDAFX_H__8495B5E0_67FF_11D4_A358_00104B732442__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 #define VC_EXTRALEAN // Exclude rarely-used stuff from Windows headers #define _WIN32_WINNT 0x0400 #include < afxwin.h > // MFC core and standard components #include < afxext.h > // MFC extensions #include < afxdisp.h > // MFC Automation classes #include < afxdtctl.h > // MFC support for Internet Explorer 4 Common Controls #ifndef _AFX_NO_AFXCMN_SUPPORT #include < afxcmn.h > // MFC support for Windows Common Controls #endif // _AFX_NO_AFXCMN_SUPPORT //{{AFX_INSERT_LOCATION}} // Microsoft Visual C++ will insert additional declarations immediately before // the previous line. #endif // !defined(AFX_STDAFX_H__8495B5E0_67FF_11D4_A358_00104B732442__INCLUDED_)列表 2。将
#define _WIN32_WINNT
行添加到 STDAFX.H
以便 DCOM 工作。接下来要做的就是添加一些内容,让客户端了解服务器支持的接口和其他一切。我们可以通过两种方式做到这一点:
- 使用
#import
引入服务器的**类型库**。这个文件HelloServ.tlb
是由MIDL
编译HelloServ.idl
文件时生成的。 - 使用
#include "HelloServ.h"
只包含接口的 C++ 和 C 声明。这很好,但之后你还必须在你的代码中定义服务器响应的所有 GUID。这些是CLSID_HelloWorld
、IID_IHelloWorld
、DIID_DHelloWorldEvents
和LIBID_HELLOSERVLib
。如果你使用#import
,这些都会为你完成。
我喜欢使用 #import
的想法,不仅是因为上面解释的原因,还因为它还能让你使用智能指针。不过要小心;我们有一个自定义的(也就是说,派生自 IUnknown
的)接口,我们的服务器使用它。在这个示例中,我们可以很好地使用 #import
,因为 IHelloWorld::SayHello()
方法不接受任何参数。如果 IHelloWorld::SayHello()
方法接受参数,并且它们不是 OLE 自动化兼容的类型,那么我们就必须跳过使用 #import
,因为它只能识别这些类型。但是,如果你用 [oleautomation]
属性标记你的自定义接口,并在方法中使用 OLE 自动化兼容的类型,这将起作用。
对于自定义接口,通常最好使用上面的第二种方法。但是,正如我之前所说,这次我们将继续使用 #import
,因为我们的方法不接受任何参数。这意味着我们需要将 HelloServ.tlb
文件从 HelloServ
项目文件夹复制到我们的 HelloCli
项目文件夹,然后添加一个 #import
行。在 STDAFX.H
中怎么样?我们还将添加 atlbase.h
和 afxctl.h
的 #include
行,因为这些文件为我们提供了稍后将使用的功能的**支持**。在 STDAFX.H
中做所有这些将有助于我们在构建程序时减少构建时间
#if !defined(AFX_STDAFX_H__8495B5E0_67FF_11D4_A358_00104B732442__INCLUDED_) #define AFX_STDAFX_H__8495B5E0_67FF_11D4_A358_00104B732442__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 #define VC_EXTRALEAN // Exclude rarely-used stuff from Windows headers #define _WIN32_WINNT 0x0400 #include < afxwin.h > // MFC core and standard components #include < afxext.h > // MFC extensions #include < afxdisp.h > // MFC Automation classes #include < afxdtctl.h > // MFC support for Internet Explorer 4 Common // Controls #ifndef _AFX_NO_AFXCMN_SUPPORT #include < afxcmn.h > // MFC support for Windows Common Controls #endif // _AFX_NO_AFXCMN_SUPPORT #include < atlbase.h > // Support for CComPtr< > #include < afxctl.h > // MFC support for Connection Points #import "HelloServ.tlb" no_namespace named_guids raw_interfaces_only //{{AFX_INSERT_LOCATION}} // Microsoft Visual C++ will insert additional declarations immediately before // the previous line. #endif // !defined(AFX_STDAFX_H__8495B5E0_67FF_11D4_A358_00104B732442__INCLUDED_)列表 3。向
STDAFX.H
添加其他所需代码。引入连接点
注意:这仅在事件源接口是 dispinterface
时才有效,就像我们的 DHelloWorldEvents
接口一样。
接下来要做的就是使用 ClassWizard 为我们提供一个类,该类将实现我们的连接点(!)。是的,我们终于到了!要做到这一点,请打开 ClassWizard,点击 Message Maps 选项卡,点击 Add Class,然后点击 New,如下图 图 2 所示
图 2。使用 ClassWizard 添加新类。
接下来要做的就是指定我们要添加到项目中的新类。由于这个类(某种程度上)实现了接口,即 DHelloWorldEvents
dispinterface,我们将把这个类命名为 CHelloWorldEvents
类。接下来,我们指定要从此类派生自 MFC 的 CCmdTarget
类,这有助于我们进行所有 COM 实现。人们常说,“MFC 除了 OLE 和 UI 所需的内容之外,并没有真正提供任何 COM 支持。而且,它只处理 dispinterfaces。” 上面的句子都不是完全正确的。MFC 在 UI 方面非常擅长,但我见过(可以正常工作的)示例代码,其中 MFC 用于实现任何你想要的接口,甚至在具有非 dispinterface 和非 IDispatch
接口的 COM 服务器中!CCmdTarget
类是关键。
总之,废话少说。在我们可以点击 New Class 对话框中的 OK 之前,最后一件事是点击 Automation 选项按钮。这会打开 CCmdTarget
中我们需要使用的支持;不用担心,选择此项甚至不会向你的项目添加一个 .ODL 文件,而且你不需要在 AppWizard 中选中“Automation”来使用它。当 New Class 对话框中的所有设置都正确时,它应该如下图 图 3 所示
图 3。在 ClassWizard 中为我们的新 CHelloWorldEvents
类指定设置。
ClassWizard 会将 CHelloWorldEvents
类添加到你的项目中,但它会抱怨,因为你在 AppWizard 中没有指定 Automation 支持。由于你没有,你的项目就没有 HelloCli.odl
文件。这对于 ClassWizard 来说太糟糕了;它会显示下面的抗议消息,但你可以点击 OK 并忽略它
图 4。ClassWizard 应该成熟点,停止抱怨;但,唉……忽略此警告并点击 OK。
更改 CHelloWorldEvents
ClassWizard 虽然有用,但它犯了一个小错误,我们需要纠正。打开 HelloWorldEvents.cpp
文件,删除下面 加粗 显示的代码行
BEGIN_MESSAGE_MAP(CHelloWorldEvents, CCmdTarget) //{{AFX_MSG_MAP(CHelloWorldEvents) // NOTE - the ClassWizard will add and remove mapping macros here. //}}AFX_MSG_MAP END_MESSAGE_MAP() BEGIN_DISPATCH_MAP(CHelloWorldEvents, CCmdTarget) //{{AFX_DISPATCH_MAP(CHelloWorldEvents) // NOTE - the ClassWizard will add and remove mapping macros here. //}}AFX_DISPATCH_MAP END_DISPATCH_MAP() // Note: we add support for IID_IHelloWorldEvents to support typesafe binding // from VBA. This IID must match the GUID that is attached to the // dispinterface in the .ODL file. // {B0652FB5-6E0F-11D4-A35B-00104B732442} static const IID IID_IHelloWorldEvents = { 0xb0652fb5, 0x6e0f, 0x11d4, { 0xa3, 0x5b, 0x0, 0x10, 0x4b, 0x73, 0x24, 0x42 } };列表 4。删除 加粗 显示的代码行。
接下来,找到下面 列表 5 中加粗显示的代码。我们将用 DHelloWorldEvents
接口的 DIID(**D**isp**I**nterface**ID**)替换它
BEGIN_INTERFACE_MAP(CHelloWorldEvents, CCmdTarget) INTERFACE_PART(CHelloWorldEvents, IID_IHelloWorldEvents, Dispatch) END_INTERFACE_MAP()列表 5。要查找的代码,加粗显示。
将 IID_IHelloWorldEvents
替换为 DIID_DHelloWorldEvents
,如 列表 6 所示
BEGIN_INTERFACE_MAP(CHelloWorldEvents, CCmdTarget) INTERFACE_PART(CHelloWorldEvents, DIID_DHelloWorldEvents, Dispatch) END_INTERFACE_MAP()列表 6。用
DIID_DHelloWorldEvents
替换 Class-Wizard 添加的 IID_IHelloWorldEvents
标识符。现在,我们将使用 ClassWizard 添加处理函数,该函数会在服务器触发 OnSayHello
事件时被调用。打开 ClassWizard,然后选择 Automation 选项卡。确保在 Class Name 框中选中了 CHelloWorldEvents
,如下图 图 5 所示
图 5。在 ClassWizard 的 Automation 选项卡上选择 CHelloWorldEvents
类。
点击 Add Method。Add Method 对话框出现,如下图 图 6 所示。在这里,我们将指定事件处理函数方法的“外部名称”,以及其他信息。“外部名称”应始终与我们在服务器中添加事件方法时使用的名称匹配!Internal Name(内部名称)框应包含当事件传入时将被调用的成员函数的名称;这个名称可以是你喜欢的任何名称。我们将使用 ClassWizard 建议的名称;一个与 OnSayHello
“外部名称”匹配的名称。始终为事件处理函数的返回类型指定 void
,因为服务器总是使用 HRESULT
作为其返回类型。对于客户端,任何时候处理连接点事件,返回类型都应始终为 void
。接下来,为事件处理函数指定一个参数,LPCTSTR lpszHost
作为该事件处理函数的唯一参数。你注意到 BSTR
不在事件处理函数类型列表中(好吧!)。这是因为你改用 LPCTSTR
;ClassWizard 会确保 MFC 在 BSTR
和 LPCTSTR
之间为你转换(!)
图 6。设置 OnSayHello
事件的处理程序。
点击 OK。ClassWizard 会向 CHelloWorldEvents
添加代码以实现魔法(在 CCmdTarget
的帮助下),然后会在其 External Names 列表框中显示一个新的条目,表示事件处理程序已添加
图 7。ClassWizard 显示添加了新的事件处理程序。
保存你的更改!确保点击 ClassWizard 中的 OK,否则它将回滚它所做的所有更改。如果你点击 Cancel,你将不得不重新添加事件处理程序。 仅供警告。现在是时候实现我们的事件处理程序了。我们将从 lpszHost
中获取服务器计算机的名称,然后向用户显示一个消息框,告诉他们服务器说了 Hello。我们可能还想向对话框的状态窗口添加文本,说明事件处理函数已被调用。我就是这样做的
#include "HelloCliDlg.h" void CHelloWorldEvents::OnSayHello(LPCTSTR lpszHost) { CHelloCliDlg* pDlg = (CHelloCliDlg*)AfxGetMainWnd(); if (pDlg != NULL) { pDlg->m_strStatus += "The OnSayHello() connection point method has been called\r\n"; pDlg->UpdateData(FALSE); } // Show a message box saying 'Hello, world, from host ' + lpszHost: CString strMessage = "Hello, world, from "; strMessage += lpszHost; AfxMessageBox(strMessage, MB_ICONINFORMATION); }列表 7。实现
CHelloWorldEvents::OnSayHello()
事件处理函数。我还必须向 CHelloWorldEvents
的声明添加一个 friend
语句,因为 CWnd::UpdateData()
是一个 protected
函数
class CHelloWorldEvents : public CCmdTarget { friend class CHelloCliDlg; ... };
接下来要做的是向 CHelloCliDlg
类添加一些数据成员,以存储我们在处理服务器时将使用的指针和对象。有很多这些,你必须确保添加以下行
#include "HelloWorldEvents.h"到
HelloCliDlg.h
文件的顶部// Implementation protected: HICON m_hIcon; DWORD m_dwCookie; // Cookie to keep track of connection point BOOL m_bSinkAdvised; // Were we able to advise the server? IHelloWorldPtr* m_pHelloWorld; // Pointer to the IHelloWorld interface pointer IUnknown* m_pHelloWorldEventsUnk; // Pointer to the IUnknown of the // event "sink" CHelloWorldEvents m_events; // Our event-handler object列表 8。我们需要添加到
CHelloCliDlg
类的数据成员。接下来,我们需要在对话框的构造函数中添加代码
CHelloCliDlg::CHelloCliDlg(CWnd* pParent /*=NULL*/) : CDialog(CHelloCliDlg::IDD, pParent) { ... m_dwCookie = 0; m_pHelloWorldEventsUnk = m_events.GetIDispatch(FALSE); // So we don't have to call Release() m_bSinkAdvised = FALSE; }列表 9。要添加到
CHelloCliDlg::CHelloCliDlg()
构造函数中的代码。为了通知服务器,我使用 ClassView 向 CHelloCliDlg
添加了一个 AdviseEventSink()
,protected
成员函数。每当你想通知服务器关于我们的 MFC 连接点时,此函数的实现都基本相同
BOOL CHelloCliDlg::AdviseEventSink() { if (m_bSinkAdvised) return TRUE; IUnknown* pUnk = NULL; CComPtr< IUnknown > spUnk = (*m_pHelloWorld); pUnk = spUnk.p; // Advise the connection point BOOL bResult = AfxConnectionAdvise(pUnk, DIID_DHelloWorldEvents, m_pHelloWorldEventsUnk, TRUE, &m_dwCookie); return bResult; }列表 10。通知服务器的实现。这演示了如何调用
AfxConnectionAdvise()
。你必须像我们在第 6 步中所做的那样正确注册服务器,否则这将不起作用。当我们准备好再次对服务器及其触发的事件漠不关心时,我们可以调用 AfxConnectionUnadvise()
BOOL CHelloCliDlg::UnadviseEventSink() { if (!m_bSinkAdvised) return TRUE; // Get the IHelloWorld IUnknown pointer using a smart pointer. // The smart pointer calls QueryInterface() for us. IUnknown* pUnk = NULL; CComPtr< IUnknown > spUnk = (*m_pHelloWorld); pUnk = spUnk.p; if (spUnk.p) { // Unadvise the connection with the event source return AfxConnectionUnadvise(pUnk, DIID_DHelloWorldEvents, m_pHelloWorldEventsUnk, TRUE, m_dwCookie); } // If we made it here, QueryInterface() didn't work and we can't // unadvise the server return FALSE; }列表 11。使用
AfxConnectionUnadvise()
通知事件源和接收器。要实际进行方法调用,你可以看到我在示例程序中是如何实现所有这些的,以及我的 AdviseEventSink()
和 UnadviseEventSink()
在其中扮演的角色。不过,请记住,将此代码添加到 OnInitDialog()
BOOL CHelloCliDlg::OnInitDialog() { CDialog::OnInitDialog(); ... CoInitialize(NULL); CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_NONE, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, NULL); return TRUE; }列表 12。将初始化代码添加到
OnInitDialog()
。OnStartServer()
函数处理用户点击的按钮,当他们想启动服务器时(多么循环)。正如你所看到的,我有一台网络服务器名为 \\Viz-06
,我使用 DCOM 连接到了它
void CHelloCliDlg::OnStartServer() { COSERVERINFO serverInfo; ZeroMemory(&serverInfo, sizeof(COSERVERINFO)); COAUTHINFO athn; ZeroMemory(&athn, sizeof(COAUTHINFO)); // Set up the NULL security information athn.dwAuthnLevel = RPC_C_AUTHN_LEVEL_NONE; athn.dwAuthnSvc = RPC_C_AUTHN_WINNT; athn.dwAuthzSvc = RPC_C_AUTHZ_NONE; athn.dwCapabilities = EOAC_NONE; athn.dwImpersonationLevel = RPC_C_IMP_LEVEL_IMPERSONATE; athn.pAuthIdentityData = NULL; athn.pwszServerPrincName = NULL; serverInfo.pwszName = L"\\\\Viz-06"; serverInfo.pAuthInfo = &athn; serverInfo.dwReserved1 = 0; serverInfo.dwReserved2 = 0; MULTI_QI qi = {&IID_IHelloWorld, NULL, S_OK}; ... try { m_pHelloWorld = new IHelloWorldPtr; } catch(...) { AfxMessageBox(AFX_IDP_FAILED_MEMORY_ALLOC, MB_ICONSTOP); ... return; } HRESULT hResult = CoCreateInstanceEx(CLSID_HelloWorld, NULL, CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER, &serverInfo, 1, &qi); if (FAILED(hResult)) { ... return; } m_pHelloWorld->Attach((IHelloWorld*)qi.pItf); // Now we have a live pointer to the IHelloWorld interface // on the remote host ... return; }列表 13。如何获取远程服务器上
IHelloWorld
接口的接口指针。调用该方法只需执行此语句
HRESULT hResult = (*m_pHelloWorld)->SayHello();列表 14。调用
IHelloWorld::SayHello()
方法。要释放服务器,当我们完成它时,只需 delete
m_pHelloWorld
指针
delete m_pHelloWorld; m_pHelloWorld = NULL;列表 15。
结束……我的朋友……
好了!现在我们有了一个活生生的 DCOM 客户端/服务器软件系统。它并没有做太多事情,但它能做很多事情……总之,我希望本教程能够让你受益匪浅,并且 DCOM 能够得到揭秘。我随时鼓励你给我发电子邮件,问我问题。没有愚蠢的问题,我很乐意帮助你。如果你想回到第 6 步,请点击下面的“返回”,或者点击“问题和解答”看看是否有人问了你需要答案的问题。
下次见……很开心。
历史
- 2003 年 12 月 22 日 - 更新下载