构建本地 COM 服务器和客户端:分步示例






4.83/5 (44投票s)
2004年10月29日
8分钟阅读

175769

4859
一步一步介绍如何构建您自己的本地 COM 服务器和客户端。
引言
许多 CP(CodeProject)会员都写过关于 COM 的文章,为什么还要再写一篇?
大多数人学习 COM 的方式是先构建一个 InprocServer
(即 DLL 文件),然后继续编写本地 COM 服务器和客户端。此时,一个展示本地 COM 服务器和客户端的简单示例将非常有帮助。但是,信不信由你,我花了数小时在网上搜索,试图找到一个本地 COM 服务器和客户端的小例子,以便在此基础上构建我自己的系统,但我从未找到!我找到的大多数示例都是 Inproc
服务器。在某些情况下,我找到了本地服务器,但文档只列出了基本步骤/思路,而没有提供代码。事实上,从了解步骤到构建一个实际的服务器还有很长的路要走:您必须编写 idl 文件、reg 文件,您必须将不同的文件包含在正确的位置(甚至按正确的顺序!您会看到的),……,任何一个错误都会导致非常令人困惑的错误消息或结果。然后我找到了 Andrew Troelsen 的书,并尝试按照他的例子来做。然而,我将 CD 中的服务器代码移到我的本地 PC 后,却发现他的本地服务器甚至无法编译——我最终弄清楚了原因(如果您继续阅读就会看到),并且我相信他在书中应该提到这个问题——这样可以为他的读者节省大量的沮丧和搜索时间。同时,他也没有提供客户端代码,所以这还不是一个完整的故事。
因此,我决定写下所有您需要遵循的必要步骤、您需要编写的所有实际代码、您需要创建的所有文件,以及您在决定构建本地 COM 服务器和客户端时需要记住的所有事情——这个小例子可以作为您更伟大、更宏伟(也更具野心)的项目的一个起点。尽管如此,我还是想清楚地说明,一半的功劳应该归于 Andrew Troelsen:对于服务器端,我使用了他的示例和他的代码(已修正问题),如果您想了解更多关于这部分的内容,可以查看他的书。
因此,如果您有兴趣构建本地 COM 服务器和客户端,这个示例可能会对您有所帮助。另外,我假设您已经具备了 COM 的基本知识:架构、接口、COM 运行时服务函数,以及您可以用来获取 GUID
和检查您的组件等的各种工具,等等。在下一节中,我们将构建一个本地 COM 服务器,之后再构建一个客户端。祝您愉快!
分步:构建本地 COM 服务器
本节将介绍构建本地服务器所需的步骤。这个简单的服务器包含一个名为 MyCar
的 coclass
以及以下三个接口:ICreateMyCar
、IEngine
和 IStats
。整个服务器本身就很能说明问题,那么就开始吧!
步骤 1:启动 VC6.0 并创建一个新的 WIN32 应用程序工作区,选择“一个简单的应用程序”,并将其命名为 CarLocalServer(或您想要的任何名称)。
步骤 2:创建您的 idl 文件,它应该看起来像下面这样,并将其命名为 CarLocalServerTypeInfo.idl
import "oaidl.idl"; // define IStats interface [object, uuid(FE78387F-D150-4089-832C-BBF02402C872), oleautomation, helpstring("Get the status information about this car")] interface IStats : IUnknown { HRESULT DisplayStats(); HRESULT GetPetName([out,retval] BSTR* petName); }; // define the IEngine interface [object, uuid(E27972D8-717F-4516-A82D-B688DC70170C), oleautomation, helpstring("Rev your car and slow it down")] interface IEngine : IUnknown { HRESULT SpeedUp(); HRESULT GetMaxSpeed([out,retval] int* maxSpeed); HRESULT GetCurSpeed([out,retval] int* curSpeed); }; // define the ICreateMyCar interface [object, uuid(5DD52389-B1A4-4fe7-B131-0F8EF73DD175), oleautomation, helpstring("This lets you create a car object")] interface ICreateMyCar : IUnknown { HRESULT SetPetName([in]BSTR petName); HRESULT SetMaxSpeed([in] int maxSp); }; // library statement [uuid(957BF83F-EE5A-42eb-8CE5-6267011F0EF9), version(1.0), helpstring("Car server with typeLib")] library CarLocalServerLib { importlib("stdole32.tlb"); [uuid(1D66CBA8-CCE2-4439-8596-82B47AA44E43)] coclass MyCar { [default] interface ICreateMyCar; interface IStats; interface IEngine; }; };
将此文件插入您的项目中。另外,请注意,所有 GUID
都应该使用 guidgen.exe 生成,因此您的 ID 将不会像上面文件中的那样。我上面列出了该文件的每一行,以便您对这个简单的服务器有一个清晰的了解。对于服务器中的其余文件,您可以下载并仔细阅读。
步骤 3:将以下文件插入您的工作区:CarLocalServer.cpp、MyCar.cpp、MyCarClassFactory.cpp、Locks.cpp、MyCar.h、MyCarClassFactory.h 和 Locks.h。现在,请注意这些您真正需要关注的事情(这两件事在 Troelsen 的书中没有提到,但它们是我无法编译的原因)
- 请注意包含的 .h 文件,并记住,每当您需要包含 stdafx.h 文件时,都要在包含任何其他文件之前先包含它。如果您不这样做,您的编译器将给出一些非常令人困惑的错误消息(至少我的编译器是这样)。
- 打开 stdafx.h,检查该文件是否包含以下行
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
如果包含,请确保将其删除(注释掉)。这也是我无法成功编译服务器的另一个原因(我花了一段时间才弄清楚)。
我真的认为 Troelsen 应该提到这两件事。嗯,如果您恰好读了他的书,我希望您也能注意到这篇文章,它可能会为您节省一些时间。
步骤 4:创建 .reg 文件。您可以为该文件取任何您想要的名称,但其内容必须与您从本文下载的完全相同,除了
- 您应该使用您刚生成的 ID,并且,
- 您应该用您自己的路径替换我在该文件中使用的路径,
- 您可以使用不同的名称(保持
coclass
名称MyCar
不变)。
创建此文件后,双击它,您的 Windows 系统应该会告诉您您的组件已成功注册。
步骤 5:构建整个项目。您应该没有任何编译错误,并请记住您至少应该运行一次。
现在您已经拥有了一个本地 COM 服务器。我再说一遍,这部分代码来自 Troelsen 的书,我只做了一些小的更正,如步骤 3 中所示(以消除编译错误)。此外,我在服务器中还添加了一些错误保护;如果出现问题,您可以得到一些有意义的错误消息。
3. 分步:构建本地 COM 客户端
现在,让我们按照以下步骤构建一个本地 COM 客户端。我将在此处列出源代码并解释重要部分,因为 Troelsen 在他的书中没有提供客户端代码。
步骤 1:再次启动您的 VC6.0 并创建一个新的 WIN32 控制台应用程序工作区。选择“一个空项目”,并将其命名为 CarLocalClient
(或您想要的任何名称)。
步骤 2:创建一个新的 .cpp 文件作为您的客户端代码,随意命名,并将其添加到项目中。您的客户端代码应如下所示
#include "../CarLocalServer/CarLocalServerTypeInfo.h" // use your own path here #include "../CarLocalServer/CarLocalServerTypeInfo_i.c" // use your own path here #include "iostream.h" // for showing possible mistakes void ShowErrorMessage(LPCTSTR,HRESULT); int main() { // initialize the COM runtime cout << "Initialize the COM runtime..."; CoInitialize(NULL); cout << "success." << endl; // declare variables HRESULT hr; IClassFactory* pICF = NULL; ICreateMyCar* pICreateMyCar = NULL; IEngine* pIEngine = NULL; IStats* pIStats = NULL; cout << endl << "Get the class factory interface for the Car class..."; hr = CoGetClassObject(CLSID_MyCar,CLSCTX_LOCAL_SERVER, NULL,IID_IClassFactory,(void**)&pICF); if ( FAILED(hr) ) { ShowErrorMessage("CoGetClassObject()",hr); exit(1); } else cout << "success." << endl; cout << "Create the Car object and get back the ICreateMyCar interface..."; hr = pICF->CreateInstance(NULL,IID_ICreateMyCar,(void**)&pICreateMyCar); if ( FAILED(hr) ) { ShowErrorMessage("CoCreateInstance()",hr); exit(1); } else cout << "success." << endl; // set parameters on the car cout << endl << "Set different parameters on the car..."; pICreateMyCar->SetMaxSpeed(30); BSTR carName = SysAllocString(OLESTR("COMCar?!")); pICreateMyCar->SetPetName(carName); SysFreeString(carName); cout << "success." << endl; cout << endl << "Query the IStats interface..."; pICreateMyCar->QueryInterface(IID_IStats,(void**)&pIStats); cout << "success." << endl; cout << endl << "Use the IStats interface to display the status of the car:" << endl; pIStats->DisplayStats(); cout << endl << "Query the IEngine interface..."; pICreateMyCar->QueryInterface(IID_IEngine,(void**)&pIEngine); cout << "success." << endl; cout << endl << "Start to use the engine..." << endl; int curSp = 0; int maxSp = 0; pIEngine->GetMaxSpeed(&maxSp); do { pIEngine->SpeedUp(); pIEngine->GetCurSpeed(&curSp); cout << "current speed is: " << curSp << endl; } while (curSp <= maxSp); if ( pICF ) pICF->Release(); if ( pICreateMyCar) pICreateMyCar->Release(); if ( pIStats ) pIStats->Release(); if ( pIEngine ) pIEngine->Release(); cout << endl << "Close the COM runtime..."; CoUninitialize(); cout << "success." << endl; return 0; } void ShowErrorMessage(LPCTSTR header, HRESULT hr) { void* pMsg; FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,NULL,hr, MAKELANGID(LANG_NEUTRAL,SUBLANG_DEFAULT), (LPTSTR)&pMsg,0,NULL); cout << header << ": Error(" << hex << hr << "): " << (LPTSTR)pMsg << endl; LocalFree(pMsg); }
请注意 include
语句:您实际上不需要包含 windows.h 之类的文件,您只需要包含 CarLocalServerTypeInfo.h 和 CarLocalServerTypeInfo_i.c 文件,而这两个文件已经包含了所有必需的头文件。请注意,这两个文件是在 VC6.0 编译 CarLocalServerTypeInfo.idl 文件后由您的系统生成的,并且它们位于您在第 2 节中创建的工作区中。因此,您需要将它们复制到您的客户端工作区目录,或者您可以使用相对路径,就像我在这里做的那样。
要访问您的 COM 组件(您的 coclass 和接口),您首先调用 COM 服务函数 CoGetClassObject()
,并从该调用中获取指向您 IClassFactory
接口的指针。一旦您拥有了这个接口,其余的似乎就很明显了:然后您就可以调用该接口上的 CreateInstance()
来开始您的旅程。您可能已经注意到 CoGetClassObject()
调用中的 CLSTX_LOCAL_SERVER
参数,这就是我们表明我们正在构建本地服务器的地方!
另一种选择是调用 CoCreateInstance()
而不是 CoGetClassObject()
。该函数实际上调用 CoGetClassObject()
,然后像我们在客户端代码中所做的那样再次调用 CreateInstance()
。也许您应该这样做,就像我们在客户端代码中所做的那样:除了性能考虑之外,这样做似乎更安全:通过自己进行两次调用,您可以在每次调用后进行错误保护,因此如果出现问题,您就能相对清楚地知道问题出在哪里。另一方面,如果您使用 CoCreateInstance()
,如果此调用失败,您将失去对问题所在位置的控制。请注意,在此客户端代码中,我们只显示错误,没有添加任何异常处理代码——我只是假设您将是添加适当异常处理的人。
步骤 3:构建项目并尝试一下。您应该能够“与”您的 COM 服务器“交谈”,而无需将其作为进程内 DLL 文件。
以上就是构建本地 COM 服务器和客户端的基本步骤。如果您真的尝试一下,您会发现这确实涉及大量工作,而且,同样,任何一个错误都会给您带来非常令人困惑的错误消息。也许,在尝试了这个示例之后,您会开始更欣赏 ATL。
就这些!
本文介绍了一个构建您自己的本地 COM 服务器和客户端的简单示例。其真正的好处(至少我希望如此)是您可以将其用作构建更实用、更大的项目的起点。您可能希望在实际工作中 YaATL,但理解底层原理总是好的。感谢您的阅读,我非常期待您的评论和建议。