WCF学习之旅—HTTP双工模式(二十)
WCF学习之旅—请求与答复模式和单向模式(十九)
四、HTTP双工模式
双工模式建立在上文所实现的两种模式的基础之上,实现客户端与服务端相互调用:前面介绍的两种方法只是在客户端调用服务端的方法,然后服务端有返回值返回客户端;相互调用是指客户端调用服务端的方法,同时服务端也可以调用客户端的方法。
基于双工MEP (信息交换模式,Message Exchange Pattern,下同)消息交换可以看成是多个基本模式下 (比如请求-回复模式和单项模式)消息交换的组合。双工MEP又具有一些变体,比如典型的订阅-发布模式就可以看成是双工模式的一种表现形式。
一) 两种典型的双工MEP
1.请求过程中的回调
这是一种比较典型的双工消息交换模式的表现形式,客户端在进行服务调用的时候,附加上一个回调对象;服务在对处理该处理中,通过客户端附加的回调对 象(实际上是调用回调服务的代理对象)回调客户端的操作(该操作在客户端执行)。整个消息交换的过程实际上由两个基本的消息交换构成,其一是客户端正常的服务请求,其二则是服务端对客户端的回调。两者可以采用请求-回复模式,也可以采用单向(One-way)的MEP进行消息交换。图1描述了这样的过程,服务调用和回调都采用请求-回复。
图1 请求过程中的回调
2.订阅-发布
订阅-发布模式是双工模式的一个典型的变体。在这个模式下,消息交换的双方变成了订阅者和发布者,若干订阅者就某个主题向发布者申请订阅,发布者将 所有的订阅者保存在一个订阅者列表中,在某个时刻将主题发送给该主题的所有订阅者。实际上基于订阅-发布模式的消息交换也可以看成是两个基本模式下消息交 换的组合,申请订阅是一个单向模式的消息交换(如果订阅者行为得到订阅的回馈,该消息交换也可以采用请求-回复模式);而主题发布也是一个基于单向模式的消息交换过程。订阅-发布消息交换模式如图2所示。
图2 订阅-发布
二) 实例演示:创建基于双工通信的WCF应用
接下来我们通过一个的实例来学习基于双工通信的WCF应用。为简单起见,我们沿用上文( WCF学习之旅—请求与答复模式和单向模式(十九))的示例。在上文的示例中,我们都是调用 BookService直接显示书籍名称,并将结果通过Winform应用程序输出。在本例中我们将采用另外一种截然不同的方式调用服务并进行结果的输出:我们通过单向(One-way)的模式调用BookService(也就是客户端不可能通过回复消息得到结果),服务端在完成书籍名称显示之后,通过回调(Callback)的方式在客户端将书籍名称显示出来。
步骤一:定义服务契约和回调契约
首先进行服务契约的定义,我们照例通过接口(IBookService)的方式定义服务契约,作用于指定显示书籍名称的方法DisplayName操作,我们通过OperationContractAttribute特性的IsOneway属性将操作定义成单向的操作,这意味着客户端仅仅是向服务端发送一个请求,并不会通过回复消息得到任何结果。
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.ServiceModel; using System.Text; namespace WcfServiceLib { // 注意: 使用“重构”菜单上的“重命名”命令,可以同时更改代码和配置文件中的接口名“IBookService”。 [ServiceContract(CallbackContract = typeof(ICallback))] public interface IBookService { /// <summary> /// 单工模式,显示名称 /// </summary> /// <param name="name">书籍名称</param> [OperationContract(IsOneWay = true)] void DisplayName(string name); } }
我们要实现的功能是通过在服务端回调客户端的操作方式实现结果的输出。客户端正常调用BookService的服务方法,那么在服务 执行过程中借助于客户端在服务调用时提供的回调对象对客户端的操作进行回调,从本质上讲是另外一种形式的服务调用。WCF采用基于服务契约的调用形式,客户端正常的服务调用需要服务契约,同理服务端回调客户端依然需要通过描述回调操作的服务契约,我们把这种服务契约称为回调契约。回调契约的类型通过ServiceContractAttribute特性的CallbackContract属性进行指定。
上面代码中服务契约IBookService的回调契约ICallback定义如下。由于回调契约本质也是一个服务契约,所以定义方式和一般意义上的 服务契约基本一样。有一点不同的是,由于定义IBookService的时候已经通过 [ServiceContract(CallbackContract=typeof(ICallback))]指明ICallback是一个服务契约 了,所以ICallback不再需要添加ServiceContractAttribute特性。ICallback定义了一个服务操作DisplayResult用于显示书籍名称与日期,由于服务端不需要回调的返回值,所以将回调操作也设为单向方法。ICallback接口代码如下。
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.ServiceModel; using System.Text; namespace WcfServiceLib { public interface ICallback { [OperationContract(IsOneWay = true)] void DisplayResult(string result); } }
步骤二:实现服务
在BookService实现了上面定义的服务契约IBookService中的方法,实现了DisplayName操作,完成书籍名称和日期的显示工作。同时把书籍名称与日期的显示通过回调的方式在客户端显示出来,所以需要借助于客户端提供的回调对象(该对象在客户端调用BookService的时候指定,在介绍客户端代码的实现的时候会讲到)。在WCF中,回调对象通过当前OperationContext的GetCallback<T>方法获得(T代表回调契约的类型)。代码如下。
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.ServiceModel; using System.Text; namespace WcfServiceLib { // 注意: 使用“重构”菜单上的“重命名”命令,可以同时更改代码、svc 和配置文件中的类名“BookService”。 // 注意: 为了启动 WCF 测试客户端以测试此服务,请在解决方案资源管理器中选择 BookService.svc 或 BookService.svc.cs,然后开始调试。 public class BookService : IBookService { /// <summary> /// 双工模式,回调显示结果 /// </summary> /// <param name="name">名称</param> public void DisplayName(string name) { string result=string.Format("书籍名称:{0},日期时间{1}", name, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); Console.WriteLine("\r\n" + result); ICallback call = OperationContext.Current.GetCallbackChannel<ICallback>(); call.DisplayResult("回调---"+result); } } }
注: OperationContext在WCF中是一个非常重要、也是一个十分有用的对象,它代表服务操作执行的上下文。我们可以通过静态属性 Current(OperationContext.Current)得到当前的OperationContext。借助 OperationContext,我们可以在服务端或者客户端获取或设置一些上下文,比如在客户端可以通过它为出栈消息(outgoing message)添加SOAP报头,以及HTTP报头(比如Cookie)等。在服务端,则可以通过OperationContex获取在客户端设置的 SOAP报头和HTTP报头。关于OperationContext的详细信息,可以参阅MSDN在线文档。
步骤三:服务寄宿
我们通过一个控制台应用程序完成对BookService的寄宿工作,并将所有的服务寄宿的参数定义在代码中。由于双工通信依赖于一个双工的信道栈,即依赖于一个能够支持双工通信的绑定,在此我们选用了WSDualHttpBinding。
using System; using System.Collections.Generic; using System.Linq; using System.ServiceModel; using System.ServiceModel.Description; using System.Text; using System.Threading.Tasks; using WcfServiceLib; namespace ConsoleHosting { class Program { static void Main(string[] args) { //创建宿主的基地址 Uri baseAddress = new Uri("http://localhost:8080/BookService"); //创建宿主 using (ServiceHost host = new ServiceHost(typeof(BookService), baseAddress)) { //向宿主中添加终结点 host.AddServiceEndpoint(typeof(IBookService), new WSDualHttpBinding (), baseAddress); if (host.Description.Behaviors.Find<ServiceMetadataBehavior>() == null) { //将HttpGetEnabled属性设置为true ServiceMetadataBehavior behavior = new ServiceMetadataBehavior(); behavior.HttpGetEnabled = true; behavior.HttpGetUrl = baseAddress; //将行为添加到Behaviors中 host.Description.Behaviors.Add(behavior); //打开宿主 host.Opened += delegate { Console.WriteLine("BookService控制台程序寄宿已经启动,HTTP监听已启动....,按任意键终止服务!"); }; host.Open(); //print endpoint information Console.ForegroundColor = ConsoleColor.Yellow; foreach (ServiceEndpoint se in host.Description.Endpoints) { Console.WriteLine("[终结点]: {0}\r\n\t[A-地址]: {1} \r\n\t [B-绑定]: {2} \r\n\t [C-协定]: {3}", se.Name, se.Address, se.Binding.Name, se.Contract.Name); } Console.Read(); } } } } }
注: 在WCF预定义绑定类型中,WSDualHttpBinding和NetTcpBinding均提供了对双工通信的支持,但是两者在对双工通信的实现机制上却有本质的区别。WSDualHttpBinding是基于HTTP传输协议的;而HTTP协议本身是基于请求-回复的传输协议,基于HTTP的通道本质上都是单向的。WSDualHttpBinding实际上创建了两个通道,一个用于客户端向服务端的通信,而另一个则用于服务端到客户端的通信,从而间接地提供了双工通信的实现。而NetTcpBinding完全基于支持双工通信的TCP协议。
步骤四:实现回调契约
在客户端程序为回调契约提供实现,在下面的代码中BookCallBack实现了回调契约ICallback,在DisplayResult方法中对书籍名称与日期的显示的输出。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace WinClient { class BookCallBack : BookServiceReference.IBookServiceCallback { //声明一个delegate(委托)类型:delegateDispalyResult,该类型可以搭载返回值为空,参数只有一个(string型)的方法。 public delegate void delegateDispalyResult(string result); //声明一个delegateDispalyResult类型的对象。该对象代表了返回值为空,参数只有一个(string型)的方法。它可以搭载N个方法。 public delegateDispalyResult mainThread; public void DisplayResult(string result) { mainThread(result);//通过委托的方法在主界面中显示书籍名称与日期信息 Console.WriteLine( result); } } }
步骤五:服务调用
接下来实现对双工服务的调用,这是一个通过Web 服务引用,添加相应的WCF服务调用对象(关于如何添加Web服务引用请参考前面的文章)。在服务调用程序中,先创建回调对象,并通过InstanceContext对回调对象进行包装,然后把InstanceContext对象做为参数传递给服务调用对象。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.ServiceModel; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace WinClient { public partial class Form1 : Form { BookCallBack call; public Form1() { InitializeComponent(); //创建BookCallBack类的对象 BookCallBack call = new BookCallBack(); call.mainThread += new BookCallBack.delegateDispalyResult(DisplayResult); instanceContext = new InstanceContext(call); } InstanceContext instanceContext; private void buttonTwoWay_Click(object sender, EventArgs e) { BookServiceReference.BookServiceClient client = new BookServiceReference.BookServiceClient(instanceContext); //在BookCallBack对象的mainThread(委托)对象上搭载两个方法,在线程中调用mainThread对象时相当于调用了这两个方法。 textBox1.Text += string.Format("开始调用wcf服务:{0}\r\n\r\n", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); client.DisplayName("科学可以这样看丛书"); textBox1.Text += string.Format("\r\n\r\n调用结束:{0}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); } /// <summary> /// 在界面上显示回调结果 /// </summary> /// <param name="result"></param> private void DisplayResult(string result) { //判断该方法是否被主线程调用,也就是创建labMessage1控件的线程,当控件的InvokeRequired属性为ture时,说明是被主线程以外的线程调用。如果不加判断,会造成异常 if (this.textBox1.InvokeRequired) { //再次创建一个BookCallBack类的对象 call = new BookCallBack(); //为新对象的mainThread对象搭载方法 call.mainThread += new BookCallBack.delegateDispalyResult(DisplayResult); //this指窗体,在这调用窗体的Invoke方法,也就是用窗体的创建线程来执行mainThread对象委托的方法,再加上需要的参数(i) this.Invoke(call.mainThread, new object[] { result }); } else { textBox1.Text +="\r\n"+ result; } } } }
在服务寄宿程序启用的情况下,运行客户端程序后,通过服务端执行的运算结果会通过回调客户端的操作显示出来,下面是最终输出的结果。