前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >服务器是干嘛的[服务器和客户端区别]

服务器是干嘛的[服务器和客户端区别]

作者头像
Java架构师必看
发布2022-03-08 13:24:47
7.2K0
发布2022-03-08 13:24:47
举报
文章被收录于专栏:Java架构师必看

大家好,我是架构君,一个会写代码吟诗的架构师。今天说一说服务器是干嘛的[服务器和客户端区别],希望能够帮助大家进步!!!

今天有个网友问我如何编写一个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_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.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。

代码下载:

/Files/killmyday/CSharpDCOMClientDemo.zip

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-03-072,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • DCOM服务器
  • C#客户端
    • 生成Interop Assembly
    • 运行
    相关产品与服务
    日志服务
    日志服务(Cloud Log Service,CLS)是腾讯云提供的一站式日志服务平台,提供了从日志采集、日志存储到日志检索,图表分析、监控告警、日志投递等多项服务,协助用户通过日志来解决业务运维、服务监控、日志审计等场景问题。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档