双工(Duplex)模式的消息交换方式体现在消息交换过程中,参与的双方均可以向对方发送消息。基于双工MEP消息交换可以看成是多个基本模式下(比如请求-回复模式和单项模式)消息交换的组合。双工MEP又具有一些变体,比如典型的订阅-发布模式就可以看成是双工模式的一种表现形式。双工消息交换模式使服务端回调(Callback)客户端操作成为可能。

一、两种典型的双工MEP

1.请求过程中的回调

这是一种比较典型的双工消息交换模式的表现形式,客户端在进行服务调用的时候,附加上一个回调对象;服务在对处理该处理中,通过客户端附加的回调对象(实际上是调用回调服务的代理对象)回调客户端的操作(该操作在客户端执行)。整个消息交换的过程实际上由两个基本的消息交换构成,其一是客户端正常的服务请求,其二则是服务端对客户端的回调。两者可以采用请求-回复模式,也可以采用单向(One-way)的MEP进行消息交换。图1描述了这样的过程,服务调用和回调都采用请求-回复MEP。


图1 请求过程中的回调

2.订阅-发布

订阅-发布模式是双工模式的一个典型的变体。在这个模式下,消息交换的双方变成了订阅者和发布者,若干订阅者就某个主题向发布者申请订阅,发布者将所有的订阅者保存在一个订阅者列表中,在某个时刻将主题发送给该主题的所有订阅者。实际上基于订阅-发布模式的消息交换也可以看成是两个基本模式下消息交换的组合,申请订阅是一个单向模式的消息交换(如果订阅者行为得到订阅的回馈,该消息交换也可以采用请求-回复模式);而主题发布也是一个基于单向模式的消息交换过程。订阅-发布消息交换模式如图2所示。

图2 订阅-发布

二、实例演示:创建基于双工通信的WCF应用

接下来我们通过一个的案例演示基于双工通信的WCF应用。为简单起见,我们沿用计算服务的例子。在这之前,我们都是调用CalculuateService直接得到计算结果,并将计算结果通过控制台输出。在本例中我们将采用另外一种截然不同的方式调用服务并进行结果的输出:我们通过单向(One-way)的模式调用CalculuateService(也就是客户端不可能通过回复消息得到计算结果),服务端在完成运算结果后,通过回调(Callback)的方式在客户端将计算结果打印出来。整个应用的层次仍然采用我们一贯的4层结构:Contracts、Services、Hosting和Clients,如图3所示。

图3 双工通信案例应用结构

步骤一:定义服务契约和回调契约

首先进行服务契约的定义,我们照例通过接口(ICalculator)的方式定义服务契约,作用于指定加法运算的Add操作,我们通过OperationContractAttribute特性的IsOneway属性将操作定义成单向的操作,这意味着客户端仅仅是向服务端发送一个运算的请求,并不会通过回复消息得到任何运算结果。

   1: using System.ServiceModel;

   2: namespace Artech.DuplexServices.Contracts

   3: {

   4:     [ServiceContract(Namespace="http://www.artech.com/",

   5: CallbackContract=typeof(ICallback))]

   6:     public interface ICalculator

   7:     {

   8:         [OperationContract(IsOneWay=true)]

   9:         void Add(double x, double y);

  10:     }

  11: }

我们试图实现的是通过在服务端回调客户端操作的方式实现运算结果的输出。客户端调用CalculatorService正常的服务调用,那么在服务执行过程中借助于客户端在服务调用时提供的回调对象对客户端的操作进行回调,从本质上讲是另外一种形式的服务调用。WCF采用基于服务契约的调用形式,客户端正常的服务调用需要服务契约,同理服务端回调客户端依然需要通过描述回调操作的服务契约,我们把这种服务契约称为回调契约。回调契约的类型通过ServiceContractAttribute特性的CallbackContract属性进行指定。

上面代码中服务契约ICalculator的回调契约ICallback定义如下。由于回调契约本质也是一个服务契约,所以定义方式和一般意义上的服务契约基本一样。有一点不同的是,由于定义ICalculator的时候已经通过[ServiceContract(CallbackContract=typeof(ICallback))]指明ICallback是一个服务契约了,所以ICallback不再需要添加ServiceContractAttribute特性。ICallback定义了一个服务操作DisplayResult用于显示运算结果(前两个参数为执行加法运算的操作数),由于服务端不需要回调的返回值,索性将回调操作也设为单向方法。

   1: using System.ServiceModel;

   2: namespace Artech.DuplexServices.Contracts

   3: {

   4:     public interface ICallback

   5:     {

   6:         [OperationContract(IsOneWay=true)]

   7:         void DisplayResult(double x, double y, double result);

   8:     }

   9: }

步骤二:实现服务

在实现了上面定义的服务契约ICalculator的服务CalculatorService中,实现了Add操作,完成运算和结果显示的工作。结果显示是通过回调的方式实现的,所以需要借助于客户端提供的回调对象(该对象在客户端调用CalculatorService的时候指定,在介绍客户端代码的实现的时候会讲到)。在WCF中,回调对象通过当前OperationContext的GetCallback<T>方法获得(T代表回调契约的类型)。

   1: using Artech.DuplexServices.Contracts;

   2: using System.ServiceModel;

   3: namespace Artech.DuplexServices.Services

   4: {

   5:     public class CalculatorService : ICalculator

   6:     {

   7:         #region ICalculator Members

   8:  

   9:         public void Add(double x, double y)

  10:         {

  11:             double result = x + y;

  12:             ICallback callback = OperationContext.Current.GetCallbackChannel<ICallback>();

  13:             callback.DisplayResult(x, y, result);

  14:         }

  15:  

  16:         #endregion

  17:     }

  18: } 

注: OperationContext在WCF中是一个非常重要、也是一个十分有用的对象,它代表服务操作执行的上下文。我们可以通过静态属性Current(OperationContext.Current)得到当前的OperationContext。借助OperationContext,我们可以在服务端或者客户端获取或设置一些上下文,比如在客户端可以通过它为出栈消息(outgoing message)添加SOAP报头,以及HTTP报头(比如Cookie)等。在服务端,则可以通过OperationContex获取在客户端设置的SOAP报头和HTTP报头。关于OperationContext的详细信息,可以参阅MSDN在线文档。

步骤三:服务寄宿

我们通过一个控制台应用程序完成对CalculatorService的寄宿工作,并将所有的服务寄宿的参数定义在配置文件中。由于双工通信依赖于一个双工的信道栈,即依赖于一个能够支持双工通信的绑定,在此我们选用了NetTcpBinding。

   1: <?xml version="1.0" encoding="utf-8" ?>

   2: <configuration>

   3:     <system.serviceModel>

   4:         <behaviors>

   5:         <services>

   6:             <service name="Artech.DuplexServices.Services.CalculatorService">

   7:                 <endpoint address="net.tcp://127.0.0.1:9999/CalculatorService"

   8:                     binding="netTcpBinding" contract="Artech.DuplexServices.Contracts.ICalculator" />

   9:             </service>

  10:         </services>

  11: </system.serviceModel>

  12: </configuration>

注: 在WCF预定义绑定类型中,WSDualHttpBindingNetTcpBinding均提供了对双工通信的支持,但是两者在对双工通信的实现机制上却有本质的区别。WSDualHttpBinding是基于HTTP传输协议的;而HTTP协议本身是基于请求-回复的传输协议,基于HTTP的通道本质上都是单向的。WSDualHttpBinding实际上创建了两个通道,一个用于客户端向服务端的通信,而另一个则用于服务端到客户端的通信,从而间接地提供了双工通信的实现。而NetTcpBinding完全基于支持双工通信的TCP协议。

   1: using System;

   2: using System.ServiceModel;

   3: using Artech.DuplexServices.Services;

   4: namespace Artech.DuplexServices.Hosting

   5: {

   6:     class Program

   7:     {

   8:         static void Main(string[] args)

   9:         {

  10:             using (ServiceHost host = new ServiceHost(typeof(CalculatorService)))

  11:             {

  12:                 host.Open();

  13:                 Console.Read();

  14:             }

  15:         }

  16:     }

  17: }

步骤四:实现回调契约

在客户端程序为回调契约提供实现,在下面的代码中CalculateCallback实现了回调契约ICallback,在DisplayResult方法中对运算结果进行输出。

   1: using System;

   2: using Artech.DuplexServices.Contracts;

   3: namespace Artech.DuplexServices.Clients

   4: {

   5:     class CalculateCallback:ICallback

   6:     {

   7:  

   8:         public void DisplayResult(double x, double y, double result)

   9:         {

  10:             Console.WriteLine("x + y = {2} when x = {0} and y = {1}", x, y, result);

  11:         }

  12:     }

  13: }

步骤五:服务调用

接下来实现对双工服务的调用,下面是相关的配置和托管程序。在服务调用程序中,通过DuplexChannelFactory<TChannel>创建服务代理对象,DuplexChannelFactory<TChannel>和ChannelFactory<TChannel>的功能都是一个服务代理对象的创建工厂,不过DuplexChannelFactory<TChannel>专门用于基于双工通信的服务代理的创建。在创建DuplexChannelFactory<TChannel>之前,先创建回调对象,并通过InstanceContext对回调对象进行包装。

   1: <?xml version="1.0" encoding="utf-8" ?>

   2: <configuration>

   3:     <system.serviceModel>

   4:         <client>

   5:             <endpoint name="CalculatorService" address="net.tcp://127.0.0.1:9999/CalculatorService" binding="netTcpBinding" contract="Artech.DuplexServices.Contracts.ICalculator" />

   6:         </client>

   7:     </system.serviceModel>

   8: </configuration>

   1: using System;

   2: using Artech.DuplexServices.Contracts;

   3: using System.ServiceModel;

   4: namespace Artech.DuplexServices.Clients

   5: {

   6:     class Program

   7:     {

   8:         static void Main(string[] args)

   9:         {

  10:             InstanceContext instanceContext = new InstanceContext(new CalculateCallback());

  11:             using(DuplexChannelFactory<ICalculator> channelFactory = new  DuplexChannelFactory<ICalculator>(instanceContext,"CalculatorService"))

  12:             {

  13:                 ICalculator proxy = channelFactory.CreateChannel();

  14:                 using (proxy as IDisposable)

  15:                 {

  16:                     proxy.Add(1, 2);

  17:                     Console.Read();

  18:                 }

  19:             }

  20:         }

  21:     }

  22: }

在服务寄宿程序启用的情况下,运行客户端程序后,通过服务端执行的运算结果会通过回调客户端的操作显示出来,下面是最终输出的结果。

x + y = 3 when x = 1 and y = 2

三、特别注意

接下来我们将针对上面这个案例,讨论一些关于双工服务的细节性问题。

问题1:回调对双工信道的依赖

在本案例中,由于使用的NetTcpBinding,所以我们底层采用的是TCP协议。由于TCP协议是一个基于连接的传输协议,只有当通信双方的连接被成功创建出来后,他们之间才能进行正常的消息传输。

在上面给出的客户端代码中,在调用了Add方法后添加了这样的语句“Console.Read();”,这是为了阻止调用proxy的Dispose方法,因为该方法将会试图关闭底层的TCP连接。由于服务端的回调操作也会使用该TCP连接,如果在回调操作尚未执行完毕就试图关闭网络连接,将会导致回调无法正常执行。所以如果我们将该语句去掉,将会抛出如图4所示的ProtocolException异常。

   1: InstanceContext instanceContext = new InstanceContext(new CalculateCallback());

   2: using(DuplexChannelFactory<ICalculator> channelFactory = new  DuplexChannelFactory<ICalculator>(instanceContext,"CalculatorService"))

   3: {

   4:     ICalculator proxy = channelFactory.CreateChannel();

   5:     using (proxy as IDisposable)

   6:     {

   7:         proxy.Add(1, 2);

   8:         //Console.Read();

   9:     }

  10: }

图4 关闭服务代理导致的ProtocolException异常

问题2:回调导致的死锁

第2个问题是关于并发的问题,我们先看表现出来的现象,再分析原因并找出解决方案。现在我们修改一下回调契约,将OperationContractAttribute的IsOneWay属性去掉,将Add操作由单向操作改成传统意义的请求-回复服务操作。运行系统,将会抛出如图5所示的InvalidOperationException异常。

   1: using System.ServiceModel;

   2: namespace Artech.DuplexServices.Contracts

   3: {

   4:     public interface ICallback

   5:     {

   6:         [OperationContract]

   7:         void DisplayResult(double x, double y, double result);

   8:     }

   9: }

图5 双工通信的并发、死锁

异常的消息已经道出了出错的原因和解决方案,不过可能是由于Visual Studio汉化的原因,显示的出错消息显得有点不知所以。究其本质,这是一个死锁导致的异常,由于默认的情况是服务的执行按Single并发模式进行,也就是说在服务执行全程,服务对象只能被一个线程访问。WCF通过加锁机制保证服务对象的独占性使用,也就是说在服务执行开始会对服务对象加锁,该锁在服务操作结束之后释放。

回到我们的例子,在Add操作执行过程中,服务端回调客户端操作进行运算结果的显示工作。如果回调是采用单向操作,回调请求一经发送便会返回,服务操作可以继续得到执行直到操作正常结束。但是服务采用请求-回复模式的回调,服务端会一直等待回调操作的返回。而另一方面,当回调操作在客户端正常执行后,回到服务端试图访问服务操作的时候,发现对象被服务操作执行的线程锁住,所以它会等待服务操作的执行完成后将锁释放。这样,服务操作需要等待回调操作进行正常返回以便执行后续操作,而回调操作只有等待服务操作执行完毕将锁释放才能得以返回,从而形成了死锁。

解决方法就是通过服务行为改变服务执行的并发模式,在下面的代码中我们在服务类型(CalculatorService)中通过ServiceBehaviorAttribute特性的ConcurrencyMode属性将并发模式设为Reentrant或者Multiple均可以解决这个问题。关于WCF中的并发是一个重要而且复杂的话题,本书的下卷会对其进行单独的介绍。

   1: [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)]

   2: public class CalculatorService : ICalculator

   3: {

   4:     //省略实现

   5: }

   1: [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple)]

   2: public class CalculatorService : ICalculator

   3: {

   4:     //省略实现

   5: }

问题3:如果采用WsDualHttpBinding?

接下来我们来看关于双工服务的第3个问题。我们这个案例采用NetTcpBinding作为终结点的绑定类型。现在我们采用基于HTTP的WSDualHttpBinding看看我们的应用能否正常运行。我们需要做的仅仅是改变服务端和客户端的配置。

   1: <?xml version="1.0" encoding="utf-8" ?>

   2: <configuration>

   3:     <system.serviceModel>

   4:         <behaviors>

   5:         <services>

   6:             <service name="Artech.DuplexServices.Services.CalculatorService">

   7:                 <endpoint address="http://127.0.0.1:9999/CalculatorService"

   8:                     binding="wsDualHttpBinding" contract="Artech.DuplexServices.Contracts.ICalculator" />

   9:             </service>

  10:         </services>

  11: </system.serviceModel>

  12: </configuration>

   1: <?xml version="1.0" encoding="utf-8" ?>

   2: <configuration>

   3:     <system.serviceModel>

   4:         <client>

   5:             <endpoint name="CalculatorService" address=" 

   6: http://127.0.0.1:9999/CalculatorService" binding=" wsDualHttpBinding" contract="Artech.DuplexServices.Contracts.ICalculator" />

   7:         </client>

   8:     </system.serviceModel>

   9: </configuration>

如果你的IIS的版本是V6或者V7,你的程序运行将一切正常。但是如果还在使用XP操作系统,使用IIS 5.X,会抛出如图6所示的AddressAlreadyInUseException异常。

图6 II 5.x + WsDualHttpBinding导致的AddressAlreadyInUseException异常

该异常的出现和不同版本的IIS监听机制有关。之所以相同的应用在使用基于TCP传输的NetTcpBinding的时候不会出现问题,那是因为HTTP和TCP它们有一个根本的区别,TCP本身就是一个双工模式的传输协议,而HTTP协议本质只能提供单向通信方式。WSDualHttpBinding通过创建两个单项信道的方式提供双工通信的实现。

对于一个双工通信的WCF服务来说,回调过程本质上也是一种服务调用,是对寄宿于客户端的回调服务的调用。为了保证回调的正常运行,在客户端创建通道的时候(比如上面的代码通过DuplexChannelFactory的CreateChannel方法的时候),会进行回调服务的寄宿,并指定回调服务的监听地址。在默认的情况下该监听地址采用这样的格式:http://hostname:80/{临时监听地址}/guid/。

由于回调的服务监听地址采用的默认端口是80,在IIS 5.x以及之前的版本中,80端口是IIS独占的监听端口。所以才会出现AddressAlreadyInUseException异常并提示地址被另外一个应用使用,实际上80端口被IIS使用。由于IIS 6和IIS 7采用基于HTTP.SYS驱动的监听方式实现了端口的共享,故而不会出现上面的问题。关于不同版本的IIS实现机制,可以参考《WCF技术剖析(卷1)第7章的有关IIS服务寄宿的内容。

由于问题的症结在于回调服务的监听端口和IIS冲突,所以我们只要能够解决这种冲突,就能从根本上解决这个问题。由于我们不可以为了解决这个问题把IIS卸掉,或者改变IIS默认的端口,所以我们只能改变回调服务的地址。WsDualHttpBinding定义了一个ClientBaseAddress使你能很容易地改变回调服务的基地址。对于我们给出的案例,我们只要通过下面的配置将clientBaseAddress设为可用的地址(http://127.0.0.1:8888/ CalculatorService),我们的问题就会迎刃而解。

   1: <?xml version="1.0" encoding="utf-8" ?>

   2: <configuration>

   3:     <system.serviceModel>

   4:         <bindings>

   5:             <wsDualHttpBinding>

   6:                 <binding name="MyBinding" clientBaseAddress="http://127.0.0.1:8888/calculatecallback" />

   7:             </wsDualHttpBinding>

   8:         </bindings>

   9:         <client>

  10:             <endpoint address="http://127.0.0.1:9999/CalculatorService" binding="wsDualHttpBinding"

  11:              bindingConfiguration="MyBinding" contract="Artech.DuplexServices.Contracts.ICalculator"

  12:              name="CalculatorService" />

  13:         </client>

[转载]WCF实现双工通信的更多相关文章

  1. WCF 双工通信

    注释:本学习是参考Artech大神的资料: 在WCF 实现双工通信 在这里我就不介绍双工通信的概念了,我写博客的目的是检测自己掌握情况,看我wcf通信后,觉得纸上得来终觉浅,绝知此事要躬行. 我使用的 ...

  2. WCF双工通信笔记

    1,Dupex(双工) MEP(消息交换模式),服务端回调(Callback)客户端操作 2,客户端调用服务时,附加上一个回调对象(InstanceContext).服务端处理服务请求时,通过该回调对 ...

  3. Wcf 双工通信的应用

    概述 双工(Duplex)模式的消息交换方式体现在消息交换过程中,参与的双方均可以向对方发送消息.基于双工MEP消息交换可以看成是多个基本模式下(比如请求-回复模式和单项模式)消息交换的组合.双工ME ...

  4. 我的WCF之旅(3):在WCF中实现双工通信

    双工(Duplex)模式的消息交换方式体现在消息交换过程中,参与的双方均可以向对方发送消息.基于双工MEP消息交换可以看成是多个基本模式下(比如请求-回复模式和单项模式)消息交换的组合.双工MEP又具 ...

  5. 在WCF中实现双工通信

    双工(Duplex)模式的消息交换方式体现在消息交换过程中,参与的双方均可以向对方发送消息.基于双工MEP消息交换可以看成是多个基本模式下(比如请求-回复模式和单项模式)消息交换的组合.双工MEP又具 ...

  6. WCF三种通信模式

    WCF在通信过程中有三种模式:请求与答复.单向.双工通信. 请求与答复模式 描述:客户端发送请求,然后一直等待服务端的响应(异步调用除外),期间处于假死状态,直到服务端有了答复后才能继续执行其他程序 ...

  7. WCF 宿主与通信模式(二)

    宿主 每个WCF服务都必须托管在Windows进程中,该进程称为宿主进程(host process) 单个宿主进程可以托管多个服务,相同的服务类型也可以托管在多个宿主进程中. wcf中托管服务一般有一 ...

  8. 如何使用HTML5的WebSocket实现网页与服务器的双工通信(二)

    本系列服务端双工通信包括两种实现方式:一.使用Socket构建:二.使用WCF构建.本文为使用WCF构建服务端的双工通信,客户端同样使用Html5的WebSocket技术进行调用. 一.创建WCF服务 ...

  9. 如何使用HTML5的WebSocket实现网页与服务器的双工通信(一)

    本系列服务端双工通信包括两种实现方式:一.使用Socket构建:二.使用WCF构建.本文为使用Socket构建服务端的双工通信,客户端同样使用Html5的WebSocket技术进行调用. 一.网页客户 ...

随机推荐

  1. elcipse 安装svn插件 转载

    1.下载最新的Eclipse,我的版本是3.7.2 indigo(Eclipse IDE for Java EE Developers)版    如果没有安装的请到这里下载安装:http://ecli ...

  2. LTE Module User Documentation(翻译2)——配置LTE MAC 调度器

    LTE用户文档 (如有不当的地方,欢迎指正!) 5 配置 LTE MAC 调度器   这里有几种 LTE MAC 调度器用户可以选择.使用下面的代码定义调度器的类型: Ptr<LteHelper ...

  3. jQuery.validate.js+API_cn

      名称 返回类型 描述 validate(options) 返回:Validator 验证所选的FORM valid() 返回:Boolean 检查是否验证通过 rules() 返回:Options ...

  4. Getuserpassword

    将[新注册的用户的用户名和密码]保存到服务端本地 /*将注册成功的用户名和密码保存到本地*/ /*定位*/ File f = new File("D:/lab_2/用户名和密码.qq&quo ...

  5. linux学习笔记2-命令总结3

    文件搜索命令 1.文件搜索命令 find 2.其他文件搜索命令 grep - 在文件中搜索字串匹配的行并输出 locate - 在文件资料库中查找文件 whereis - 搜索命令所在目录及帮助文档路 ...

  6. poj1673EXOCENTER OF A TRIANGLE

    链接 据说这题是垂心..数学太弱没有看出来,写了分朴实无华的代码.. 旋转三边得到图中的外顶点,然后连接三角形顶点求交点,交上WA..觉得没什么错误就去看了下discuss,发现都在说精度问题,果断开 ...

  7. Android 让输入框输入指定字符的办法

    让输入框输入指定字符的办法 有一个需求 让输入密码的时候只能输入数字字母可见字符 不能输入中文 之前还以为要写代码 还来发现有一个属性可以直接实现 <EditText android:layou ...

  8. iOS--获取输入字符的第一个字母(汉字则获取拼音的第一个字母)

    - (NSString *)firstCharactor:(NSString *)aString { //转成了可变字符串 NSMutableString *str = [NSMutableStrin ...

  9. MVC HtmlHelper用法大全

    MVC HtmlHelper用法大全HtmlHelper用来在视图中呈现 HTML 控件.以下列表显示了当前可用的一些 HTML 帮助器. 本主题演示所列出的带有星号 (*) 的帮助器. ·Actio ...

  10. 转!数据库连接池概念、种类、配置(DBCP\C3P0\JndI与Tomact配置连接池)

    数据库连接池概念.种类.配置(DBCP\C3P0\JndI与Tomact配置连接池) 一.DBCP 连接:DBCP 连接池是 Apache 软件基金组织下的一个开源连接池实现. 需要的 java 包c ...