65.9K
CodeProject 正在变化。 阅读更多。
Home

DCOM 揭秘:DCOM 教程,第 7 步

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (38投票s)

2000 年 8 月 7 日

GPL3
viewsIcon

912690

downloadIcon

4653

终于!我们通过使用 MFC、AppWizard 和 ClassWizard 编写客户端来结束我们的教程,就像在那些美好的日子里一样(叹息……)

Image of the HelloCli sample program
图 1。HelloCli 示例程序的图像。

引言

欢迎来到我们的 DCOM 教程的第 7 步。这是最后一步!

如果你想跟着本教程学习,并在此过程中添加代码和使用 Visual C++ 向导,那就太好了。事实上,我非常、非常强烈地推荐这样做,否则本教程将是电子墨水(?)的大量浪费。但是,我在写教程的过程中,也是严格按照教程进行的,并且就像我告诉你们应该做的那样,开发代码并使用 Visual C++ 向导。事实上,截图就是我为每个步骤开发文件时的截图!要下载这些已经开发好的代码以与你自己的代码进行比较,只需点击每个步骤顶部的“下载第 n 步文件 - n KB”链接。在教程的问题和解答页面,还有一个所有步骤文件的存档。我仍然建议你跟着我们一起学习;这样,你就可以在编码的同时学习。如果你在学习过程中遇到任何问题,请随时

请记住,本教程中我们开发软件的步骤如下:

  • 第 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_HelloWorldIID_IHelloWorldDIID_DHelloWorldEventsLIBID_HELLOSERVLib。如果你使用 #import,这些都会为你完成。

我喜欢使用 #import 的想法,不仅是因为上面解释的原因,还因为它还能让你使用智能指针。不过要小心;我们有一个自定义的(也就是说,派生自 IUnknown 的)接口,我们的服务器使用它。在这个示例中,我们可以很好地使用 #import,因为 IHelloWorld::SayHello() 方法不接受任何参数。如果 IHelloWorld::SayHello() 方法接受参数,并且它们不是 OLE 自动化兼容的类型,那么我们就必须跳过使用 #import,因为它只能识别这些类型。但是,如果你用 [oleautomation] 属性标记你的自定义接口,并在方法中使用 OLE 自动化兼容的类型,这将起作用。

对于自定义接口,通常最好使用上面的第二种方法。但是,正如我之前所说,这次我们将继续使用 #import,因为我们的方法不接受任何参数。这意味着我们需要将 HelloServ.tlb 文件从 HelloServ 项目文件夹复制到我们的 HelloCli 项目文件夹,然后添加一个 #import 行。在 STDAFX.H 中怎么样?我们还将添加 atlbase.hafxctl.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 所示

Adding a new class with ClassWizard.
图 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 所示

Specifying the settings for our new CHelloWorldEvents class in ClassWizard.
图 3。在 ClassWizard 中为我们的新 CHelloWorldEvents 类指定设置。

ClassWizard 会将 CHelloWorldEvents 类添加到你的项目中,但它会抱怨,因为你在 AppWizard 中没有指定 Automation 支持。由于你没有,你的项目就没有 HelloCli.odl 文件。这对于 ClassWizard 来说太糟糕了;它会显示下面的抗议消息,但你可以点击 OK 并忽略它

ClassWizard should just grow up, and quit its whining, but oh well...
图 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 所示

Selecting the CHelloWorldEvents class on the Automation tab.
图 5。在 ClassWizard 的 Automation 选项卡上选择 CHelloWorldEvents 类。

点击 Add Method。Add Method 对话框出现,如下图 图 6 所示。在这里,我们将指定事件处理函数方法的“外部名称”,以及其他信息。“外部名称”应始终与我们在服务器中添加事件方法时使用的名称匹配!Internal Name(内部名称)框应包含当事件传入时将被调用的成员函数的名称;这个名称可以是你喜欢的任何名称。我们将使用 ClassWizard 建议的名称;一个与 OnSayHello“外部名称”匹配的名称。始终为事件处理函数的返回类型指定 void,因为服务器总是使用 HRESULT 作为其返回类型。对于客户端,任何时候处理连接点事件,返回类型都应始终为 void。接下来,为事件处理函数指定一个参数,LPCTSTR lpszHost 作为该事件处理函数的唯一参数。你注意到 BSTR 不在事件处理函数类型列表中(好吧!)。这是因为你改用 LPCTSTR;ClassWizard 会确保 MFC 在 BSTRLPCTSTR 之间为你转换(!)

Setting up a handler for the OnSayHello event.
图 6。设置 OnSayHello 事件的处理程序。

点击 OK。ClassWizard 会向 CHelloWorldEvents 添加代码以实现魔法(在 CCmdTarget 的帮助下),然后会在其 External Names 列表框中显示一个新的条目,表示事件处理程序已添加

ClassWizard showing the addition of our new event handler.
图 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 日 - 更新下载

<< 返回

问答

© . All rights reserved.