C++ DCOM服务器和C#客户端互操作完全解释
今天有个网友问我如何编写一个DCOM 服务器 ,可以在C#的客户端中调用。看起来还是有很多人在用COM技术,趁这个机会,就把DCOM和C#之间的互操作好好讲讲。
实际上,C#调用DCOM服务器的时候,只需要在C#这边做一些手脚,对于原先的C++ DCOM服务器来说,是不需要做任何改动的。道理很简单,C#后于C++ DCOM技术出现,作为前辈的DCOM技术不可能预知采用什么技术支持小辈C#。在C#里面使用DCOM的服务,跟 C++的COM客户端的步骤是一样的,即:
1. 查询注册表,启动CLSID对应的COM服务器,并激活COM对象。
2. 根据IID获取COM的指针,然后调用COM对象提供的服务。
当C#尝试调用DCOM服务的时候,实际上步骤是一样的,只不过前面两步的工作由所谓的PIA(Primary 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行定义一个CSimpleObject的CLSID,58行的Message函数用来打印一个日志,跟踪各个函数的调用。
然后将下面的键值写入注册表里面:
双击代码全选
HKEY_CLASSES_ROOTCLSID{5e9ddec7-5767-11cf-beab-00aa006c3606} = Simple Object Server
HKEY_CLASSES_ROOTCLSID{5e9ddec7-5767-11cf-beab-00aa006c3606}LocalServer32 =c:simplesserverWin32Debugsserver.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 secondsn" ),
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_ROOTAPPID{5e9ddec7-5767-11cf-beab-00aa006c3606}RemoteServerName=<机器名>
然后确保DCOM服务器端的机器的注册表里,下面的键值是“Y”:
HKEY_LOCAL_MACHINESoftwareMicrosoftOLEEnableRemoteConnect
第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.idl和ocidl.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里也会生成IStream的C#定义,但是那个定义不对。第17行到19行就是正常地通过IStream来操作DCOM对象了。第20行,程序退出的时候,CLR会自动释放掉DCOM对象的引用计数。
编译命令:
Csc /debug:full /r:DemoSimpleObjectLib.dll program.cs
运行
一切都做好了以后,要测试的话,请按照下面的步骤来操作:
1. 在一个命令行窗口中启动sserver.exe。
2. 然后启动C#客户端,在调试器中运行到第20行的时候,你会看到buffer的内容都是0xFF。
本文示例代码或素材下载
作者: Leo_wl
出处: http://www.cnblogs.com/Leo_wl/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
版权信息查看更多关于C++ DCOM服务器和C#客户端互操作完全解释的详细内容...