zoukankan      html  css  js  c++  java
  • C++ DCOM服务器和C#客户端互操作完全解释

    FROM :  http://www.cnblogs.com/killmyday/archive/2010/07/04/1770717.html

    今天有个网友问我如何编写一个DCOM服务器,可以在C#的客户端中调用。看起来还是有很多人在用COM技术,趁这个机会,就把DCOMC#之间的互操作好好讲讲。

    实际上,C#调用DCOM服务器的时候,只需要在C#这边做一些手脚,对于原先的C++ DCOM服务器来说,是不需要做任何改动的。道理很简单,C#后于C++ DCOM技术出现,作为前辈的DCOM技术不可能预知采用什么技术支持小辈C#。在C#里面使用DCOM的服务,跟 C++COM客户端的步骤是一样的,即:

    1.         查询注册表,启动CLSID对应的COM服务器,并激活COM对象。

    2.         根据IID获取COM的指针,然后调用COM对象提供的服务。

    C#尝试调用DCOM服务的时候,实际上步骤是一样的,只不过前面两步的工作由所谓的PIAPrimary Interop Assembly)做了,更精确地说,是创建了一个只包含抽象函数的类来实现的。每次C#程序调用这个类的抽象函数的时候,CLR会自动将调用转换成对应的COM调用。

    DCOM服务器

    为了简单起见,我们先来写一个最简单的DCOM服务器,这个DCOM服务器很简单,不能被客户端自动激活(自动激活的技术后面的文章讲),只能在客户端连接之前手工启动。这样做的目的,是为了让本文能够更专注的解释C#客户端使用DCOM服务器的过程—因为把COM库后台执行的操作尽可能地排除掉了。

    下面是这个DCOM服务器的源代码:

    1. #define INC_OLE2

    2. #define STRICT

    3. #include <stdio.h>

    4. #include <windows.h>

    5. #include <initguid.h>

    6.

    7. DEFINE_GUID(CLSID_SimpleObject, 0x5e9ddec7, 0x5767, 0x11cf, 0xbe, 0xab, 0x0, 0xaa, 0x0, 0x6c, 0x36, 0x6);

    8.

    9. HANDLE          hevtDone;

    10.

    11. class CClassFactory : public IClassFactory {

    12.   public:

    13.     STDMETHODIMP    QueryInterface (REFIID riid, void** ppv);

    14.     STDMETHODIMP_(ULONG) AddRef(void) { return 1; };

    15.     STDMETHODIMP_(ULONG) Release(void) { return 1; }

    16.

    17.     STDMETHODIMP    CreateInstance (LPUNKNOWN punkOuter, REFIID iid, void **ppv);

    18.     STDMETHODIMP    LockServer (BOOL fLock) { return E_FAIL; };

    19. };

    20.

    21. class CSimpleObject : public IStream {

    22.   public:

    23.     STDMETHODIMP    QueryInterface (REFIID iid, void **ppv);

    24.     STDMETHODIMP_(ULONG) AddRef(void) { return InterlockedIncrement(&m_cRef); };

    25.     STDMETHODIMP_(ULONG) Release(void) { if (InterlockedDecrement(&m_cRef) == 0) { delete this; return 0; } return 1; }

    26.

    27.     STDMETHODIMP    Read(void *pv, ULONG cb, ULONG *pcbRead);

    28.     STDMETHODIMP    Write(VOID const *pv, ULONG cb, ULONG *pcbWritten);

    29.     STDMETHODIMP    Seek(LARGE_INTEGER dbMove, DWORD dwOrigin, ULARGE_INTEGER *pbNewPosition)

    30.         { return E_FAIL; }

    31.     STDMETHODIMP    SetSize(ULARGE_INTEGER cbNewSize)

    32.         { return E_FAIL; }

    33.     STDMETHODIMP    CopyTo(IStream *pstm, ULARGE_INTEGER cb, ULARGE_INTEGER *pcbRead, ULARGE_INTEGER *pcbWritten)

    34.         { return E_FAIL; }

    35.     STDMETHODIMP    Commit(DWORD grfCommitFlags)

    36.         { return E_FAIL; }

    37.     STDMETHODIMP    Revert(void)

    38.         { return E_FAIL; }

    39.     STDMETHODIMP    LockRegion(ULARGE_INTEGER bOffset, ULARGE_INTEGER cb, DWORD dwLockType)

    40.         { return E_FAIL; }

    41.     STDMETHODIMP    UnlockRegion(ULARGE_INTEGER bOffset, ULARGE_INTEGER cb, DWORD dwLockType)

    42.         { return E_FAIL; }

    43.     STDMETHODIMP    Stat(STATSTG *pstatstg, DWORD grfStatFlag)

    44.         { return E_FAIL; }

    45.     STDMETHODIMP    Clone(IStream **ppstm)

    46.         { return E_FAIL; }

    47.

    48.     CSimpleObject()     { m_cRef = 1; }

    49.     ~CSimpleObject()    { SetEvent(hevtDone); }

    50.

    51.   private:

    52.     LONG        m_cRef;

    53. };

    54.

    55. CClassFactory   g_ClassFactory;

    56.

    57. void

    58. Message(LPTSTR szPrefix, HRESULT hr)

    59. {

    60.     LPTSTR   szMessage;

    61.

    62.     if (hr == S_OK)

    63.         {

    64.         wprintf(szPrefix);

    65.         wprintf(TEXT("\n"));

    66.         return;

    67.         }

    68. 

    69.     if (HRESULT_FACILITY(hr) == FACILITY_WINDOWS)

    70.         hr = HRESULT_CODE(hr);

    71.

    72.     FormatMessage(

    73.         FORMAT_MESSAGE_ALLOCATE_BUFFER |

    74.         FORMAT_MESSAGE_FROM_SYSTEM |

    75.         FORMAT_MESSAGE_IGNORE_INSERTS,

    76.         NULL,

    77.         hr,

    78.         MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), //The user default language

    79.         (LPTSTR)&szMessage,

    80.         0,

    81.         NULL );

    82.

    83.     wprintf(TEXT("%s: %s(%lx)\n"), szPrefix, szMessage, hr);

    84.    

    85.     LocalFree(szMessage);

    86. } // Message

    87.

    88. STDMETHODIMP

    89. CSimpleObject::QueryInterface(REFIID riid, void** ppv)

    90. {

    91.     if (ppv == NULL)

    92.         return E_INVALIDARG;

    93.     if (riid == IID_IUnknown || riid == IID_IStream)

    94.         {

    95.         *ppv = (IUnknown *) this;

    96.         AddRef();

    97.         return S_OK;

    98.         }

    99.     *ppv = NULL;

    100.     return E_NOINTERFACE;

    101. } // CSimpleObject::QueryInterface

    102.

    103. STDMETHODIMP

    104. CSimpleObject::Read(void *pv, ULONG cb, ULONG *pcbRead)

    105. {

    106.     Message(TEXT("Server: IStream:Read"), S_OK);

    107.     if (!pv && cb != 0)

    108.         return E_INVALIDARG;

    109.

    110.     // 对于读取操作,只是简单地把数组的内容设置为0xFF

    111.     if (cb != 0)

    112.         memset(pv, 0xFF, cb);

    113.

    114.     if (pcbRead)

    115.         *pcbRead = cb;

    116.     return S_OK;

    117. } // CSimpleObject::Read

    118.

    119. STDMETHODIMP

    120. CSimpleObject::Write(VOID const *pv, ULONG cb, ULONG *pcbWritten)

    121. {

    122.     Message(TEXT("Server: IStream:Write"), S_OK);

    123.     if (!pv && cb != 0)

    124.         return E_INVALIDARG;

    125.    

    126.    // 不执行任何写操作,只是简单地更新pcbWritten

    127.    // 这样客户端就会误认为写操作已经成功。

    128.     if (pcbWritten)

    129.         *pcbWritten = cb;

    130.     return S_OK;

    131. } // CSimpleObject::Write

    132.

    133. STDMETHODIMP

    134. CClassFactory::QueryInterface(REFIID riid, void** ppv)

    135. {

    136.     if (ppv == NULL)

    137.         return E_INVALIDARG;

    138.     if (riid == IID_IClassFactory || riid == IID_IUnknown)

    139.         {

    140.         *ppv = (IClassFactory *) this;

    141.         AddRef();

    142.         return S_OK;

    143.         }

    144.     *ppv = NULL;

    145.     return E_NOINTERFACE;

    146. } // CClassFactory::QueryInterface

    147.

    148. STDMETHODIMP

    149. CClassFactory::CreateInstance(LPUNKNOWN punkOuter, REFIID riid, void** ppv)

    150. {

    151.     LPUNKNOWN   punk;

    152.     HRESULT     hr;

    153.

    154.     *ppv = NULL;

    155.

    156.     if (punkOuter != NULL)

    157.         return CLASS_E_NOAGGREGATION;

    158.

    159.     Message(TEXT("Server: IClassFactory:CreateInstance"), S_OK);

    160.

    161.     punk = new CSimpleObject;

    162.

    163.     if (punk == NULL)

    164.         return E_OUTOFMEMORY;

    165.

    166.     hr = punk->QueryInterface(riid, ppv);

    167.     punk->Release();

    168.     return hr;

    169. } // CClassFactory::CreateInstance

    170.

    171. void __cdecl

    172. main()

    173. {

    174.     HRESULT hr;

    175.     DWORD   dwRegister;

    176.

    177.     // 创建一个Windows事件,等待客户端来创建

    178.    // CSimpleObject对象,模拟一个一直在线的DCOM服务器

    179.     hevtDone = CreateEvent(NULL, FALSE, FALSE, NULL);

    180.     if (hevtDone == NULL)

    181.         {

    182.         hr = HRESULT_FROM_WIN32(GetLastError());

    183.         Message(TEXT("Server: CreateEvent"), hr);

    184.         exit(hr);

    185.         }

    186.

    187.     hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);

    188.     if (FAILED(hr))

    189.         {

    190.         Message(TEXT("Server: CoInitializeEx"), hr);

    191.         exit(hr);

    192.         }

    193.

    194.     // 注册类厂

    195.     hr = CoRegisterClassObject(CLSID_SimpleObject, &g_ClassFactory,

    196.         CLSCTX_SERVER, REGCLS_SINGLEUSE, &dwRegister);

    197.     if (FAILED(hr))

    198.         {

    199.         Message(TEXT("Server: CoRegisterClassObject"), hr);

    200.         exit(hr);

    201.         }

    202.

    203.     Message(TEXT("Server: Waiting"), S_OK);

    204.

    205.    // 等待DCOM客户的请求

    206.     WaitForSingleObject(hevtDone, INFINITE);

    207.

    208.     CloseHandle(hevtDone);

    209.

    210.     CoUninitialize();

    211.     Message(TEXT("Server: Done"), S_OK);

    212. } // main

    这个DCOM服务器很简单,就是包含了一个实现了IStream接口的COM对象,类厂也在这个DCOM服务器中实现,main里面的逻辑就是当程序被手工启动以后,一直等待客户端的请求,当完成一个客户的请求以后,退出。第7行定义一个CSimpleObjectCLSID58行的Message函数用来打印一个日志,跟踪各个函数的调用。

    然后将下面的键值写入注册表里面:

    HKEY_CLASSES_ROOT\CLSID\{5e9ddec7-5767-11cf-beab-00aa006c3606} = Simple Object Server

    HKEY_CLASSES_ROOT\CLSID\{5e9ddec7-5767-11cf-beab-00aa006c3606}\LocalServer32 =c:\simple\sserver\Win32\Debug\sserver.exe

     

    C++客户端

    使用下面的C++ DCOM客户端程序来验证一下DCOM服务器:

    1. #define INC_OLE2

    2. #include <stdio.h>

    3. #include <windows.h>

    4. #include <initguid.h>

    5. #include <tchar.h>

    6. #include <conio.h>

    7.

    8. DEFINE_GUID(CLSID_SimpleObject, 0x5e9ddec7, 0x5767, 0x11cf, 0xbe, 0xab, 0x0, 0xaa, 0x0, 0x6c, 0x36, 0x6);

    9.

    10. const ULONG cbDefault = 4096;   

    11.

    12. void

    13. Message(LPTSTR szPrefix, HRESULT hr)

    14. {

    15.     LPTSTR   szMessage;

    16.

    17.     if (hr == S_OK)

    18.         {

    19.         wprintf(szPrefix);

    20.         return;

    21.         }

    22. 

    23.     if (HRESULT_FACILITY(hr) == FACILITY_WINDOWS)

    24.         hr = HRESULT_CODE(hr);

    25. 

    26.     FormatMessage(

    27.         FORMAT_MESSAGE_ALLOCATE_BUFFER |

    28.         FORMAT_MESSAGE_FROM_SYSTEM |

    29.         FORMAT_MESSAGE_IGNORE_INSERTS,

    30.         NULL,

    31.         hr,

    32.         MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

    33.         (LPTSTR)&szMessage,

    34.         0,

    35.         NULL);

    36.

    37.     wprintf(TEXT("%s: %s(%lx)\n"), szPrefix, szMessage, hr);

    38.    

    39.     LocalFree(szMessage);

    40. } // Message

    41.

    42. void

    43. OutputTime(LARGE_INTEGER* pliStart, LARGE_INTEGER* pliFinish)

    44. {

    45.     LARGE_INTEGER liFreq;

    46.

    47.     QueryPerformanceFrequency(&liFreq);

    48.     wprintf(TEXT("%0.4f seconds\n"),

    49.         (float)(pliFinish->LowPart - pliStart->LowPart)/(float)liFreq.LowPart);

    50. } // OutputTime

    51.

    52. void __cdecl

    53. main(int argc, CHAR **argv)

    54. {

    55.     HRESULT hr;

    56.     MULTI_QI    mq;

    57.     COSERVERINFO csi, *pcsi=NULL;

    58.     WCHAR wsz [MAX_PATH];

    59.     ULONG cb = cbDefault;

    60.     LARGE_INTEGER liStart, liFinish;

    61.

    62.     // 如果有参数的话,那第一个参数就是运行DCOM服务器的

    63.     // 的机器名。其实这个步骤,对于C#程序来说,是没有

    64.     // 办法支持的,但是不要着急,可以通过修改注册表

    65.     // 的方式来实现

    66.     if (argc > 1)

    67.         {

    68.         MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, argv[1], -1,

    69.             wsz, sizeof(wsz)/sizeof(wsz[0]));

    70.         memset(&csi, 0, sizeof(COSERVERINFO));

    71.         csi.pwszName = wsz;

    72.         pcsi = &csi;

    73.         }

    74.

    75.     // 第二个参数是IStream读写的字节数目

    76.     if (argc > 2)

    77.         cb = atol(argv[2]);

    78.        

    79.     hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);

    80.     if (FAILED(hr))

    81.         {

    82.         Message(TEXT("Client: CoInitializeEx"), hr);

    83.         exit(hr);

    84.         }

    85.

    86.     Message(TEXT("Client: Creating Instance..."), S_OK);

    87.     mq.pIID = &IID_IStream;

    88.     mq.pItf = NULL;

    89.     mq.hr = S_OK;

    90.     QueryPerformanceCounter(&liStart);

    91.     hr = CoCreateInstanceEx(CLSID_SimpleObject, NULL, CLSCTX_SERVER, pcsi, 1, &mq);

    92.     QueryPerformanceCounter(&liFinish);

    93.     OutputTime(&liStart, &liFinish);

    94.

    95.     if (FAILED(hr))

    96.         Message(TEXT("Client: CoCreateInstanceEx"), hr);

    97.     else

    98.         {

    99.         LPVOID      pv;

    100.         LPSTREAM    pstm = (IStream*)mq.pItf;

    101.     if (!pstm)

    102.        {

    103.        Message(TEXT("Client: NULL Interface pointer"),E_FAIL);

    104.        exit(E_FAIL);

    105.        }

    106.

    107.        // 执行读取操作

    108.         Message(TEXT("Client: Reading data..."), S_OK);

    109.         pv = CoTaskMemAlloc(cb);

    110.         QueryPerformanceCounter(&liStart);

    111.         hr = pstm->Read(pv, cb, NULL);

    112.         QueryPerformanceCounter(&liFinish);

    113.         OutputTime(&liStart, &liFinish);

    114.         if (FAILED(hr))

    115.             Message(TEXT("Client: IStream::Read"), hr);

    116.

    117.         // “写入一些数据

    118.         Message(TEXT("Client: Writing data..."), S_OK);

    119.         QueryPerformanceCounter(&liStart);

    120.         hr = pstm->Write(pv, cb, NULL);

    121.         QueryPerformanceCounter(&liFinish);

    122.         OutputTime(&liStart, &liFinish);

    123.         if (FAILED(hr))

    124.             Message(TEXT("Client: IStream::Write"), hr);

    125.

    126.         pstm->Release();

    127.         }

    128.

    129.     CoUninitialize();

    130.     Message(TEXT("Client: Done"), S_OK);

    131. } // main

    62行的代码,DCOM既然是远程服务器,那它就应该是可以运行在另外一台机器上,然后被其他机器的客户端所使用。所以C++的客户端代码里,你可以通过编程的方式指定服务器的名称,但是对于C#来说,因为连接到DCOM服务器并激活COM对象的操作是由CLR完成的,没有办法在代码里指定。不过不用着急,指定DCOM服务器还有另外一个方式,就是修改注册表的键值,告诉本机的COM运行库,服务器在另外一台机器上,请把下面的键值添加到客户端机器的注册表里:

    HKEY_CLASSES_ROOT\APPID\{5e9ddec7-5767-11cf-beab-00aa006c3606}\RemoteServerName=<机器名>

    然后确保DCOM服务器端的机器的注册表里,下面的键值是“Y”:

    HKEY_LOCAL_MACHINE\Software\Microsoft\OLE\EnableRemoteConnect

    91行代码就是激活DCOM服务器的代码了。

    C#客户端

    既然已经知道C++客户端是如何连接和激活DCOM对象以后,我们来看看在C#里面如何做,在C#里面,我们是通过下面的步骤来连接和激活DCOM对象的:

    1.         需要知道要激活的DCOM对象的CLSID,这样CLR才能让COM运行库查询注册表,启动注册表CLSID下面的LocalServer32设置的可执行程序(我们的例子里,是sserver.exe)。

    a)         至于COM运行库是如何根据CLSID启动DCOM服务器的,这篇文章里不讲,因为本文中我们的DCOM服务器是需要手工启动的。

    2.         获取已经激活的DCOM对象的指针,接着再是查询对应的COM接口,本文的例子里是IStream接口,这样在C#程序里面才能调用。但是又涉及到另外一个问题,C#是强类型语言,所有的对象调用都是要有明确的类型定义的。为了解决这个问题,我们需要在C#程序里自己定义好COM对象和接口的定义。

    为了解决上面两步操作,CLR团队提供了tlbimp.exe这个程序,这个程序需要一个类型库(.tlb)文件,从类型库中获取COM对象和接口的定义,然后将这些定义转换成C#的定义,最后将C#的定义封装到一个所谓的Interop Assembly里。因此在C#客户端,只需要引用这个Interop Assembly就可以了,关系图如下:

     

    生成Interop Assembly

    因为需要生成一个类型库(.tlb)文件,所以我们需要手工创建一个IDL文件,显示地列出DCOM对象和接口的定义,下面是这个IDL文件的定义:

    1. import "oaidl.idl";

    2. import "ocidl.idl";

    3.

    4. [

    5. uuid(7FF2526D-2672-4e13-9F95-93E9B1247B15),

    6. version(1.0),

    7. helpstring("Demo Simple Object 1.0 Type Library")

    8. ]

    9. library DemoSimpleObjectLib

    10. {

    11.     importlib("stdole2.tlb");

    12.

    13.     [

    14.         uuid(5e9ddec7-5767-11cf-beab-00aa006c3606),

    15.         helpstring("Demo Simple Class")

    16.     ]

    17.     coclass SimpleObjectClass {

    18.         [default] interface IStream;

    19.     }

    20. }

     

    因为IStream接口是COM库自带的,所以我引入了oaidl.idlocidl.idl文件,将IStream接口的定义加进来。第9行声明了一个类型库DemoSimpleObjectLib,第5行指定了类型库的GUID,这个GUID会在注册表注册这个类型库的时候用到,但我们这次不需要让COM运行库知道DemoSimpleObjectLib这个类型库,所以不会注册这个类型库。第17行列出了DCOM对象SimpleObjectClass的定义,由于这个对象只实现了一个接口,所以在18行就只列出了这个接口。注意,你不需要在DCOM对象(coclass)的定义里将对象的函数全部列出,因为COM是接口式变成,知道实现什么接口以后,就知道DCOM对象里有什么函数了。把这个文件保存为demosimpleobject.idl

     

    下一步就是生成类型库文件,并生成Interop Assembly了,毕竟C#程序不理解类型库文件,需要Interop Assembly这个中介才能跟COM打交道。下面的步骤生成类型库和Interop Assembly

     

    1.         打开Visual Studio 2008 Command Prompt窗口

    2.         执行下面的命令从IDL文件生成类型库文件:

    midl demosimpleobject.idl

    3.         执行下面的命令从类型库文件生成Interop Assembly

    tlbimp demosimpleobject.tlb

     

    Interop Assembly生成好了以后,就可以在C#程序中引用了,下面是C#的客户端源代码(program.cs):

    1. using System;

    2. using System.Collections.Generic;

    3. using System.Linq;

    4. using System.Text;

    5.

    6. using DemoSimpleObjectLib;

    7. using System.Runtime.InteropServices;

    8. using IStreamOfficial = System.Runtime.InteropServices.ComTypes.IStream;

    9.

    10. namespace CSharpClient

    11. {

    12.     class Program

    13.     {

    14.         static void Main(string[] args)

    15.         {

    16.             var stream = new SimpleObjectClassClass() as IStreamOfficial;

    17.             var cb = 4096;

    18.             var buffer = new byte[cb];

    19.             stream.Read(buffer, cb, IntPtr.Zero);

    20.         }

    21.     }

    22. }

     

    6行将Interop Assembly里面的COM对象和接口定义引入进来,第16行连接到DCOM服务器并创建一个DCOM对象,最后查询对象的IStream指针。我在第8行里将IStream重命名为IStreamOfficial,因为在Interop Assembly里也会生成IStreamC#定义,但是那个定义不对。第17行到19行就是正常地通过IStream来操作DCOM对象了。第20行,程序退出的时候,CLR会自动释放掉DCOM对象的引用计数。

     

    编译命令:

    Csc /debug:full /r:DemoSimpleObjectLib.dll program.cs

     

    运行

    一切都做好了以后,要测试的话,请按照下面的步骤来操作:

    1.         在一个命令行窗口中启动sserver.exe

    2.         然后启动C#客户端,在调试器中运行到第20行的时候,你会看到buffer的内容都是0xFF

     

     

    代码下载:

    /Files/killmyday/CSharpDCOMClientDemo.zip

  • 相关阅读:
    MyBatis 数据库字段排序问题(一)
    MySQL 函数
    Google 浏览器设置打开超链接到新窗口标签页
    Linux 命令整理 vim
    IDEA比较实用的插件之翻译插件(Translation)
    Dubbo-本地Bean测试
    Spring+dubbo错误(二)
    Spring+dubbo错误(一)
    Dubbo-本地测试直连
    上架app被拒原因总结
  • 原文地址:https://www.cnblogs.com/gmth/p/2988053.html
Copyright © 2011-2022 走看看