您的位置:首页 > 编程语言 > C语言/C++

会话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。

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

 

   

    
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  WCF C# COM