会话WCF服务 -> C# WCF COM 客户端 双工通信 -> C++通过COM调用C# WCF客户端
2013-07-21 12:34
459 查看
最近项目需要,需要实现C++客户端和WCF服务器双工通信。但C++没有好用的WCF实现框架。经过一番折腾,想出了个通过COM与C#的WCF客户端通信的方法。期间遇到各种问题,现在写下来备忘。
开发环境:WIN7,VS2010,.Net Framework CLR 3.5。
要求支持会话,回调接口是IDualCallback,实现代码如下:
在登录Login和退出Logout时,调用回调,通知客户端。
使用wsDualHttpBinding,配置如下:
这里要说明的是,MSDN中提到客户端的endpoint定义中,要写明address,其实这不是必要的,而且客户端地址不是唯一的,默认配置下,客户端会自动生成一个终结点地址,默认使用80端口。所以要防止80端口被占用,可以在客户端配置中指定地址,如:http://localhost:4321/DualCallback
1.新建 C#类库项目,注意要选择本机中安装了CLR 运行时的框架版本,如果选的版本过高,C++ COM客户端在创建COM实例时会出现0x8013101b的错误,很难定位原因。这里我选择3.5 Client Profile。
2. 在项目属性里设置程序集COM可见,生成设置中选中“为COM互操作注册”。
3.添加 WcfService 服务引用,在"高级..."对话框中设置生成类的访问级别为internal,因为我们不希望向COM公开WCF服务接口。
4.客户端的WCF配置不用更改,默认就行,当然若不能使用80端口,可以修改回调终结点配置,具体方法,这里不再赘述,网上有很多。
废话少说,上代码:
为了和C++交互,公开一些接口到COM,同事将WCF回调公开为COM事件。需要注意的是,实现双工的WCF,很容易出现死锁的现象,解决方法代码已经给出,还有不明白的,到网上搜吧,这个现象很普遍,MSDN中也有提到。
维护COM版本是很头痛的是,为了向使用普通DLL一样使用COM,我们使用Side-By-Side技术,将此客户端标识为并行程序集。方法是向项目中添加新项->在模板中选“应用程序清单文件”->删除里面的内容,换成下面的:
这些项的意义在MSDN中都有说明,publicKeyToken 和threadingModel 不是必须的,这又和MSDN中的说法不一样。其他的项就必须存在了。
一定要确保公开的COM类,能够构造成功,否则,C++ COM客户端会获得 类接口没有实现 错误。建议新建一个C# Form项目测试一下。
1.新建 MFC应用程序项目,类型为对话框。
2.使用类向导,添加 类库中的MFC类,选 文件, 选WCF客户端项目输出目录下刚生成的tlb文件,选 IMyComClass接口,类向导将生成一个类继承自COleDispatchDriver类。
OK,现在修改代码,让其支持COM,并可以响应COM事件。
首先,修改InitInstance()代码,添加以下内容,是程序支持COM,如果没有这一步,实例化COM类时会返回 类没有注册 错误。
然后,对第2步生成的 IDispatch 包装类进行修改。
声明COM事件监听类:
这里用到了ATL的IDispEventSimpleImpl模板类,你总不会想自己实现IDispatch的所有接口吧!DECLARE_EVENT_FUNC是我定义的一个宏,用来生成带参数或返回值的事件方法信息,这个信息在SINK_ENTRY_INFO宏中要被用到,对于OnNotifyUserStateStub,将生成变量名为OnNotifyUserStateStub_FuncInfo的_ATL_FUNC_INFO结构体变量。
我们用SINK_ENTRY_INFO向IDispEventSimpleImpl注册了IMyComClassEvents的DISPID_ONNOTIFYUSERSTATE事件,将该事件发生时将调用事件响应存根CMyComClassEventSink::OnNotifyUserStateStub,事件存根会调用虚函数OnNotifyUserState,继承类通过实现OnNotifyUserState,就可以响应该COM事件。
OK,就让我们的IDispatch 包装类来响应COM事件吧,添加基类CMyComClassEventSink:
在构造函数中,我们创建COM实例,并连接事件监听接口:
析构函数中断开监听:
实现事件响应:
接下来要做的,就是调用IDispatch 包装类CMyComClass。我们在按钮单击事件中调用:
现在客户端已经完成了,但要支持Side-By-Side,还要最后一步,添加程序集依赖,这可以通过向程序添加清单文件资源,或将清单文件和EXE文件放到一起,并以EXE文件名(包括扩展名)后加.manifest命名。这里我用一种更简单的方法,在stdafx.h找到下面的预处理命令:
它是被#ifdef _UNICODE包围的,当你选择多字节字符集时,它将全部灰掉,把#ifdef _UNICODE去了吧,Windows的UNICODE版本控件库是兼容多字节的,去掉不会有任何问题,这也是多字节项目使用vista样式的UI的好方法。言归正传,在它下面添加一行:
需要说明的是这里的参数应该和WCF客户端清单文件中的保持一致,否则,将出现错误,错误信息可以在事件查看器中找到。
最后,修改WCF客户端项目的输出路径和C++ COM客户端的一致,生成解决方案,到输出目录下,改WCF客户端配置文件名为C++ COM客户端名,如:ComClient.exe.config。
大功告成!调试运行,结果如下图:
细心的朋友可能会发现,两个客户端的对话框不是同时弹出的,父窗口也又问题,这大概是应为窗口实在COM的回调线程中依次弹出的,解决方法留给读者。实际上,我是测试需要,弹框提示,实际应用建议在事件响应中不要写阻塞执行的代码,保证回调及时返回,以及时通知其他客户端。当然,当客户端部在同一台机器上时是不会有这个问题的。
源码下载地址:http://download.csdn.net/detail/wuding1104/5790889,希望对有需要的朋友有所帮助。不足之处,望高手批评指正之。
开发环境:WIN7,VS2010,.Net Framework CLR 3.5。
一、WCF服务器端
服务器实现的功能比较简单。接口定义如下:namespace WcfService { // 回调接口,由客户端实现 [ServiceContract] public interface IDualCallback { [OperationContract(IsOneWay = true)] void NotifyUserState(string strName,bool bLogin); } // 服务接口 [ServiceContract(CallbackContract = typeof(IDualCallback),SessionMode = SessionMode.Required)] public interface IDualService { [OperationContract(IsInitiating = true)] int Login(string strName, string strPassword); [OperationContract] void Logout(); [OperationContract] CompositeType GetDataUsingDataContract(CompositeType composite); [OperationContract] string GetData(int nValue); } // 使用下面示例中说明的数据协定将复合类型添加到服务操作 [DataContract] public class CompositeType { bool boolValue = true; string stringValue = "Hello "; [DataMember] public bool BoolValue { get { return boolValue; } set { boolValue = value; } } [DataMember] public string StringValue { get { return stringValue; } set { stringValue = value; } } } }
要求支持会话,回调接口是IDualCallback,实现代码如下:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession,ConcurrencyMode = ConcurrencyMode.Reentrant)] public class DualService : IDualService { protected static SessionCollection m_scSessions = new SessionCollection(); public Session Session { get { return m_scSessions[OperationContext.Current.SessionId]; } } public int Login(string strName, string strPassword) { // 校验密码 // ... // 密码错误 // return -1; // 添加Session if (!m_scSessions.Contains(OperationContext.Current.SessionId)) m_scSessions.Add(new Session(OperationContext.Current)); // 记录下用户名 Session["UserName"] = strName; // 通知客户端 foreach (Session s in m_scSessions) { IDualCallback callback = Session.Context.GetCallbackChannel<IDualCallback>(); callback.NotifyUserState(strName, true); } return m_scSessions.Count(); // 返回在线人数 } public void Logout() { // 通知客户端 foreach (Session s in m_scSessions) { IDualCallback callback = Session.Context.GetCallbackChannel<IDualCallback>(); callback.NotifyUserState(Session["UserName"], false); } // 删除Session m_scSessions.Remove(OperationContext.Current.SessionId); } public CompositeType GetDataUsingDataContract(CompositeType composite) { if (composite == null) { throw new ArgumentNullException("composite"); } if (composite.BoolValue) { composite.StringValue += "Suffix"; } return composite; } public string GetData(int nValue) { if (!m_scSessions.Contains(OperationContext.Current.SessionId)) return "你还没有登录。"; return "GetData " + nValue; } }
在登录Login和退出Logout时,调用回调,通知客户端。
使用wsDualHttpBinding,配置如下:
<system.serviceModel> <bindings> <wsDualHttpBinding> <binding name="wsDualHttp" /> </wsDualHttpBinding> </bindings> <client> <endpoint binding="wsDualHttpBinding" bindingConfiguration="wsDualHttp" contract="WcfService.IDualCallback"> <identity> <dns value="localhost" /> </identity> </endpoint> </client> <services> <service name="WcfService.DualService"> <endpoint address="" binding="wsDualHttpBinding" bindingConfiguration="wsDualHttp" contract="WcfService.IDualService"> <identity> <dns value="localhost" /> </identity> </endpoint> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" /> <host> <baseAddresses> <add baseAddress="http://localhost:8732/Design_Time_Addresses/WcfService/DualService/" /> </baseAddresses> </host> </service> </services> <behaviors> <serviceBehaviors> <behavior> <!-- 为避免泄漏元数据信息, 请在部署前将以下值设置为 false 并删除上面的元数据终结点 --> <serviceMetadata httpGetEnabled="True"/> <!-- 要接收故障异常详细信息以进行调试, 请将以下值设置为 true。在部署前设置为 false 以避免泄漏异常信息--> <serviceDebug includeExceptionDetailInFaults="True" /> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel>
这里要说明的是,MSDN中提到客户端的endpoint定义中,要写明address,其实这不是必要的,而且客户端地址不是唯一的,默认配置下,客户端会自动生成一个终结点地址,默认使用80端口。所以要防止80端口被占用,可以在客户端配置中指定地址,如:http://localhost:4321/DualCallback
二 、WCF客户端
WCF客户端实现起来比较复杂,因为要向COM公开接口,供C++ COM客户端调用。1.新建 C#类库项目,注意要选择本机中安装了CLR 运行时的框架版本,如果选的版本过高,C++ COM客户端在创建COM实例时会出现0x8013101b的错误,很难定位原因。这里我选择3.5 Client Profile。
2. 在项目属性里设置程序集COM可见,生成设置中选中“为COM互操作注册”。
3.添加 WcfService 服务引用,在"高级..."对话框中设置生成类的访问级别为internal,因为我们不希望向COM公开WCF服务接口。
4.客户端的WCF配置不用更改,默认就行,当然若不能使用80端口,可以修改回调终结点配置,具体方法,这里不再赘述,网上有很多。
废话少说,上代码:
[ComVisible(false)] public delegate void NotifyUserStateDelegate(string strName, bool bLogin); [ServiceBehavior(UseSynchronizationContext = false)] internal class DualServiceCallbackHandler : IDualServiceCallback { protected MyComClass m_ccComClass = null; public DualServiceCallbackHandler(MyComClass com) { m_ccComClass = com; } public void NotifyUserState(string strName, bool bLogin) { m_ccComClass.FireNotifyUserStateEvent(strName,bLogin); } } public interface IMyComClass { int Login(string strUserName, string strPassword); void Logout(); string GetData(int nValue); } [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] public interface IMyComClassEvents { [DispId(1)] void OnNotifyUserState(string strName, bool bLogin); } [Guid("5E9C8B4C-69C3-47B4-8011-545A89F82611")] [ClassInterface(ClassInterfaceType.None)] [ComSourceInterfaces(typeof(IMyComClassEvents))] public class MyComClass : IMyComClass { public event NotifyUserStateDelegate OnNotifyUserState; internal DualServiceClient m_dscServiceClient = null; public MyComClass() { // 捕捉异常,否则,有异常发生时,COM客户端会获得没有类接口错误 try { InstanceContext icContext = new InstanceContext(new DualServiceCallbackHandler(this)); m_dscServiceClient = new DualServiceClient(icContext); } catch { } } public int Login(string strUserName, string strPassword) { return m_dscServiceClient.Login(strUserName, strPassword); } public void Logout() { m_dscServiceClient.Logout(); } public string GetData(int nValue) { return m_dscServiceClient.GetData(nValue); } public void FireNotifyUserStateEvent(string strName, bool bLogin) { OnNotifyUserState(strName, bLogin); } }
为了和C++交互,公开一些接口到COM,同事将WCF回调公开为COM事件。需要注意的是,实现双工的WCF,很容易出现死锁的现象,解决方法代码已经给出,还有不明白的,到网上搜吧,这个现象很普遍,MSDN中也有提到。
维护COM版本是很头痛的是,为了向使用普通DLL一样使用COM,我们使用Side-By-Side技术,将此客户端标识为并行程序集。方法是向项目中添加新项->在模板中选“应用程序清单文件”->删除里面的内容,换成下面的:
<?xml version="1.0" encoding="utf-8"?> <assembly manifestVersion="1.0" xmlns="urn:schemas-microsof ae4c t-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <assemblyIdentity version="1.0.0.0" name="TestCOM" publicKeyToken="5966d2d2b4ed6374" type="win32"/> <clrClass clsid="{5E9C8B4C-69C3-47B4-8011-545A89F82611}" progid="TestCOM.MyComClass" threadingModel="Both" name="TestCOM.MyComClass"></clrClass> </assembly>
这些项的意义在MSDN中都有说明,publicKeyToken 和threadingModel 不是必须的,这又和MSDN中的说法不一样。其他的项就必须存在了。
一定要确保公开的COM类,能够构造成功,否则,C++ COM客户端会获得 类接口没有实现 错误。建议新建一个C# Form项目测试一下。
三、C++ COM客户端
C++ COM客户端实现并不复杂,但创建COM类实例的时候容易失败。使用Side-By-Side并行程序集时,COM的注册不是必须的。我们先生成一遍WCF客户端项目。1.新建 MFC应用程序项目,类型为对话框。
2.使用类向导,添加 类库中的MFC类,选 文件, 选WCF客户端项目输出目录下刚生成的tlb文件,选 IMyComClass接口,类向导将生成一个类继承自COleDispatchDriver类。
OK,现在修改代码,让其支持COM,并可以响应COM事件。
首先,修改InitInstance()代码,添加以下内容,是程序支持COM,如果没有这一步,实例化COM类时会返回 类没有注册 错误。
if(!AfxOleInit()) { TRACE("Init OLE Failed."); return FALSE; }
然后,对第2步生成的 IDispatch 包装类进行修改。
声明COM事件监听类:
DECLARE_EVENT_FUNC(VT_EMPTY,OnNotifyUserStateStub,2,VT_BSTR,VT_BOOL) class CMyComClassEventSink : public IDispEventSimpleImpl<1,CMyComClassEventSink,&__uuidof(IMyComClassEvents)> { public: CMyComClassEventSink(){} public: virtual void OnNotifyUserState(LPCTSTR lpszUserName, BOOL bLogin) {} protected: void _stdcall OnNotifyUserStateStub(BSTR bstrUserName, VARIANT_BOOL bLogin) { OnNotifyUserState(CString(bstrUserName),bLogin);} public: BEGIN_SINK_MAP(CMyComClassEventSink) SINK_ENTRY_INFO(1, __uuidof(IMyComClassEvents), DISPID_ONNOTIFYUSERSTATE, &CMyComClassEventSink::OnNotifyUserStateStub,&OnNotifyUserStateStub_FuncInfo) END_SINK_MAP() };
这里用到了ATL的IDispEventSimpleImpl模板类,你总不会想自己实现IDispatch的所有接口吧!DECLARE_EVENT_FUNC是我定义的一个宏,用来生成带参数或返回值的事件方法信息,这个信息在SINK_ENTRY_INFO宏中要被用到,对于OnNotifyUserStateStub,将生成变量名为OnNotifyUserStateStub_FuncInfo的_ATL_FUNC_INFO结构体变量。
#define DECLARE_EVENT_FUNC(vtReturn,fnFunName,nParams,ParamsTypes,...) _ATL_FUNC_INFO fnFunName##_FuncInfo = {CC_STDCALL,vtReturn,nParams,{ParamsTypes,__VA_ARGS__}};
我们用SINK_ENTRY_INFO向IDispEventSimpleImpl注册了IMyComClassEvents的DISPID_ONNOTIFYUSERSTATE事件,将该事件发生时将调用事件响应存根CMyComClassEventSink::OnNotifyUserStateStub,事件存根会调用虚函数OnNotifyUserState,继承类通过实现OnNotifyUserState,就可以响应该COM事件。
OK,就让我们的IDispatch 包装类来响应COM事件吧,添加基类CMyComClassEventSink:
class CMyComClass : public COleDispatchDriver, public CMyComClassEventSink
在构造函数中,我们创建COM实例,并连接事件监听接口:
CMyComClass() { COleException ex; COleDispatchDriver::CreateDispatch(_T("TestCOM.MyComClass"),&ex); if(ex.m_sc != 0) { ex.ReportError(); return; } CMyComClassEventSink::DispEventAdvise(m_lpDispatch); } // 调用 COleDispatchDriver 默认构造函数
析构函数中断开监听:
~CMyComClass() { CMyComClassEventSink::DispEventUnadvise(m_lpDispatch); }
实现事件响应:
virtual void OnNotifyUserState(LPCTSTR lpszUserName, BOOL bLogin) { MessageBox(AfxGetMainWnd()->GetSafeHwnd(),CString(_T("用户")) + lpszUserName + (bLogin ? _T("上线了") : _T("下线了")),NULL,MB_OK); }
接下来要做的,就是调用IDispatch 包装类CMyComClass。我们在按钮单击事件中调用:
void CComClientDlg::OnBnClickedButtonLogin() { int nRet = m_pmccComClass->Login(_T("wd"),_T("***")); if(nRet < 0) MessageBox(_T("登录失败。")); else { CString strText; strText.Format(_T("登录成功,当前在线人数:%d。"),nRet); MessageBox(strText); } } void CComClientDlg::OnBnClickedButtonLogout() { m_pmccComClass->Logout(); }
现在客户端已经完成了,但要支持Side-By-Side,还要最后一步,添加程序集依赖,这可以通过向程序添加清单文件资源,或将清单文件和EXE文件放到一起,并以EXE文件名(包括扩展名)后加.manifest命名。这里我用一种更简单的方法,在stdafx.h找到下面的预处理命令:
#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='x86' publicKeyToken='6595b64144ccf1df' language='*'\"")
它是被#ifdef _UNICODE包围的,当你选择多字节字符集时,它将全部灰掉,把#ifdef _UNICODE去了吧,Windows的UNICODE版本控件库是兼容多字节的,去掉不会有任何问题,这也是多字节项目使用vista样式的UI的好方法。言归正传,在它下面添加一行:
#pragma comment(linker,"/manifestdependency:\"type='win32' name='TestCOM' version='1.0.0.0' publicKeyToken='5966d2d2b4ed6374' \"")
需要说明的是这里的参数应该和WCF客户端清单文件中的保持一致,否则,将出现错误,错误信息可以在事件查看器中找到。
最后,修改WCF客户端项目的输出路径和C++ COM客户端的一致,生成解决方案,到输出目录下,改WCF客户端配置文件名为C++ COM客户端名,如:ComClient.exe.config。
大功告成!调试运行,结果如下图:
细心的朋友可能会发现,两个客户端的对话框不是同时弹出的,父窗口也又问题,这大概是应为窗口实在COM的回调线程中依次弹出的,解决方法留给读者。实际上,我是测试需要,弹框提示,实际应用建议在事件响应中不要写阻塞执行的代码,保证回调及时返回,以及时通知其他客户端。当然,当客户端部在同一台机器上时是不会有这个问题的。
源码下载地址:http://download.csdn.net/detail/wuding1104/5790889,希望对有需要的朋友有所帮助。不足之处,望高手批评指正之。
相关文章推荐
- C\C++ Dll ->C# ->MaxScript通过C#调用C++写的Dll
- C# 调用C++的dll,通过DllImport方式。 from http://www.cnblogs.com/xiaokang088/archive/2011/04/08/2009673.html
- C#开发WEBService服务 C++开发客户端调用WEBService服务
- WCF客户端调用服务
- C#调用wcf服务
- SpringCloud(第 012 篇)电影微服务接入 Feign 进行客户端负载均衡,通过 FeignClient 调用远程 Http 微服务
- C#调用C++编写的COM DLL
- 客户端调用wcf服务,如何提高调用性能
- C++和C#编写并且相互调用COM组件
- 对硬编码WCF服务的封装(提供服务和客户端调用的封装,调用样例....)
- javaWeb服务详解【客户端调用】(含源代码,测试通过,注释) ——Dept实体类
- com调用的几种方法 及 C#调用C++编写的的COM DLL
- WCF随客户端软件一起发布,客户端自动识别WCF服务地址,不通过配置文件绑定WCF服务,客户端动态获取版本号
- COM(VB/VBA/Script)利用服务标记调用WCF服务之一使用类型化契约
- SpringCloud(第 012 篇)电影微服务接入 Feign 进行客户端负载均衡,通过 FeignClient 调用远程 Http 微服务
- C#调用C++编写的COM DLL
- C#调用C++生成的类(通过CLR类库实现)
- C++ 调用 C#写的COM (基于VS2008)
- VC使用GSOAP调用C#WCF服务
- C#调用C++编写的COM DLL