会话WCF服务 -> C# WCF COM 客户端 双工通信 -> C++通过COM调用C# WCF客户端


      最近项目需要,需要实现C++客户端和WCF服务器双工通信。但C++没有好用的WCF实现框架。经过一番折腾,想出了个通过COM与C#的WCF客户端通信的方法。期间遇到各种问题,现在写下来备忘。

     开发环境: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-microsoft-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,希望对有需要的朋友有所帮助。不足之处,望高手批评指正之。





 

   
    

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值