第四讲:服务契约
代码
https://yunpan.cn/cPns5DkGnRGNs 密码:3913
ServiceContractAttribute 与 OperationContractAttribute
ServiceContractAttribute:将一个接口转换成一个契约,每个服务契约都有一个确定的名称,当在一个接口上应用了该属性以后,默认的名称就是接口的名称。我们可以通过Name属性显示地指定需要的名称。
NameSpace:服务契约的命名空间,其作用是解决命名冲突的问题,提倡将你所在的公司名称或项目名称的URN作为命名空间。WCF默认的命名空间是:http://tempuri.org/
OperationContractAttribute:服务契约是一组相关服务操作(Operation)的集合,当我们在一个接口上应用了ServiceContractAttribute,便赋予了服务契约的属性。但是对于这样一个类型,它的成员并不会自动成为契约的服务操作,只有应用了OperationContractAttribute特性后,相应的方法成员才能成为能够通过服务调用方式访问的服务操作.
上面这一块 在前面介绍的 时候 我们已经很熟悉了 ,就是 之前定义契约的 代码中可以看到 这两个特性 ,而 NameSpace 是 改变元数据 的 命名名称 和 命名空间
同一个服务契约的所有服务操作都有一个名称,并作为其唯一的标识,该名称通过OperationContractAttribute的Name属性指定。在默认的情况下,该名称为操作对应的方法名称。
例如:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.ServiceModel; 6 7 namespace Contracts 8 { 9 [ServiceContract(Name = "CalCulator", Namespace = "http://www.hulkxu.com")] 10 public interface ICalculator 11 { 12 double Add(double x, double y); 13 } 14 }
就等于 代码:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.ServiceModel; 6 7 namespace Contracts 8 { 9 [ServiceContract(Name = "CalCulator", Namespace = "http://www.hulkxu.com")] 10 public interface ICalculator 11 { 12 13 [OperationContract(Name = "Add")] 14 double Add(double x, double y); 15 } 16 }
因为 在默认的情况下,该名称为操作对应的方法名称。 ( 我只是这么说,让大家理解,实际操作中可不会这么写 再重复 定义 name 等于 自己的操作名称 )
注意:由于名称是操作的唯一标识,所以在同一个操作契约中,不允许多个操作共享同一个名称。另一方面,由于在默认的情况下,方法名称即为操作名称,对于重载的两个服务操作,如果没有显示指定操作名称,也是不允许的。
比如下面两种服务契约的定义都是不合法的。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.ServiceModel; 6 7 namespace Contracts 8 { 9 [ServiceContract] 10 public interface ICalculator 11 { 12 [OperationContract(Name = "Add")] 13 double AddDouble(double x, double y); 不合法 14 [OperationContract(Name = "Add")] 15 double AddInt(int x, int y); 16 } 17 }
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.ServiceModel; 6 7 namespace Contracts 8 { 9 [ServiceContract] 10 public interface ICalculator 11 { 12 [OperationContract] 13 double Add(double x, double y); 14 [OperationContract] 不合法 15 double Add(int x, int y); 16 } 17 }
下面这个 虽然是 重载 但 指定的操作名称 不同 是合法的
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.ServiceModel; 6 7 namespace Contracts 8 { 9 [ServiceContract(Name="CalCulator",Namespace="http://www.HulkXu.com")] 10 public interface ICalculator 11 { 12 13 [OperationContract(Name = "AddDouble")] 合法 14 double Add(double x, double y); 15 16 [OperationContract(Name = "AddInt")] 17 double Add(int x, int y); 18 19 } 20 }
消息交换模式(MEP)与服务操作
WCF是一个完全基于消息的通信框架,服务的调用通过消息交换来实现。
1:请求-回复模式下的服务契约与操作
该模式是最常用的消息交换模式,服务的消费者向服务的提供者发送请求,并等待,接受对方的回复。服务操作在默认的情况下采用这种消息交换模式。从消息格式的角度来说,对于一个普通的操作方法,输入参数列表决定了请求消息的格式,而回复消息的格式则
由返回值类型决定。
注意:
如果我们定义的契约的返回值为Void
我们说对于请求——回复消息交换模式,输入参数列表决定输入消息的格式,而输出消息的格式通过返回值的类型决定,那么对于void这种特殊的返回类型,情况是怎样的呢?Void表示没有返回值,是否意味着通过基于void的服务操作只有请求消息,没有回复消息呢?
答案是否定的,既然采用了请求—回复的模式,就意味着请求方在发送请求后会受到回复消息,只不过回复消息的主体为空。
如:
1 [OperationContract] 2 Void Add(double x, double y);
从消息传递的角度来理解基于ref和out参数的服务操作,ref参数的值将包含在输入(请求)和输出(回复)消息中,而out参数和返回值并没有本质的不同,参数值最终作为操作结果返回到客户端。从消息结构的角度来说,ref参数的类型同时是输入消息结构的决定因素之一,而输出消息的结构除了决定于返回类型外,out和ref参数的类型是决定输出消息结构的因素之一。
如:
1 [OperationContract] 2 Void Add(double x, double y, ref double result);
2.单向(one-way)模式下的服务契约与操作
虽然在大多数情况下,我们采用请求——回复的消息交换模式调用服务并获得结果,但是,在某些情况下,我们调用某个服务并不一定有相应的结果返回,在一些极端的情况下,我们甚至不要求所有的服务调用都能成功执行。采用单向模式的服务调用,一旦消息进入网络传输层,就马上返回,此后的过程则完全和客户端无关了。客户端不但不会收到任何回复消息即使服务端抛出异常也不会被客户端感知。在定义服务契约的时候,通过OperationContractAttribute的IsOneWay属性将服务操作赋予单向消息交换的特性。由于单向消息交换是不具有任何回复消息的,单向的操作方法没有返回值,所以只有返回类型为void的方法才能将IsOneWay属性设为true.
如:
1 [OperationContract(IsOneWay = true)] 2 void WriteLog(string message);
3.双工模式下的服务契约与操作
双工模式模式的消息交换方式体现在消息交换过程中,参与的双方均可以向对方发送消息。客户端在进行服务调用的时候,附加上一个回调对象;服务在执行服务操作的过程中,通过客户端附加的回调对象(实际上是调用回调服务的代理对象)回调客户端的操作(该操作在客户端执行)。整个消息交换的过程实际上由两个基本的消息交换构成,其一是客户端正常的服务请求,其二则是服务端的客户端的回调。两者可以采用请求——回复模式,也可以采用单向模式进行消息的交换。
[ 4-01 ]
这里我们重新做一个Demo
创建基于双工通信的WCF应用的Dome
为简单起见,我们沿用计算服务的例子。在这之前,我们都是调用CalculatorService直接得到计算结果,并将计算结果通过控制台输出。该例子是:通过单向的模式调用CalculatorService(也就是客户端不可能通过回复消息得到计算结果),服务端在完成运算结果后,通过回调的方式在客户端将计算结果打印出来。
这个Demo 我就不 一步一步 教着搭建了,去云盘 上找 代码。
先看服务端的 契约
[ 4-02 ]
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.ServiceModel; 6 7 namespace Contracts 8 { 9 /// <summary> 10 /// 客户端调用CalculatorService进行正常的服务调用,那么在服务执行过程中借助于客户端在 11 /// 服务调用时提供的回调对象对客户端的操作进行回调。从本质上讲是另外一种形式的服务调用 12 /// 。WCF采用基于服务契约的调用方式,客户端正常的服务调用需要服务契约,同理服务端回调 13 /// 客户端依然需要通过描述回调操作的服务契约,我们把这种服务契约称为回调契约。回调契约 14 /// 的类型通过CallbackContract属性指定. 15 /// </summary> 16 [ServiceContract(Namespace = "http://www.HulkXu.com/", CallbackContract = typeof(ICallback))] 17 public interface ICalculator 18 { 19 [OperationContract(IsOneWay = true)] 20 void Add(double x, double y); 21 } 22 }
上面这一步是 服务端的 契约 注意 定义 接口为 服务契约的时候 打上了 特性 ServiceContract ,并且 特性中的属性 CallbackContract = typeof( 客户端的 回调契约 ) 即 4-03 图片中的接口名
**************************************************************************************************************************
[ 4-03 ]
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.ServiceModel; 6 7 namespace Contracts 8 { 9 public interface ICallback 10 { 11 /// <summary> 12 /// 由于回调契约本质上也是一个服务契约,所以定义方式和一般意义上的服务契约基本上 13 /// 一样。有一点不同的是,由于定义ICalculator的时候已经通过 [ServiceContract(CallbackContract = typeof(ICallback))] 14 /// 指明ICallback是一个服务契约了,所以ICallback不再需要添加ServiceContractAttribute特性 15 /// DisplayResult:用于显示运算结果,由于服务端不需要回调的返回值,所以回调操作也设为 16 /// 单向的模式. 17 /// </summary> 18 /// <param name="x"></param> 19 /// <param name="y"></param> 20 /// <param name="result"></param> 21 [OperationContract(IsOneWay = true)] 22 void DisplayResult(double x, double y, double result); 23 } 24 }
然后 到这里 是 服务端调用客户端的回调契约, 记得这里做的 双方都不需要返回值
**************************************************************************************************************************
再看我们的 服务端的实现:
[ 4-04 ]
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using Contracts; 6 using System.ServiceModel; 7 8 namespace Services 9 { 10 public class CalculatorService : ICalculator 11 { 12 /// <summary> 13 /// 实现了服务契约ICalculator中的Add方法。结果显示是通过回调的方式实现的, 14 /// 所以要借助于客户端提供的回调对象。在WCF中,回调对象通过当前OperationContext对象获取 15 /// </summary> 16 /// <param name="x"></param> 17 /// <param name="y"></param> 18 public void Add(double x, double y) 19 { 20 double result = x + y; 21 //得到客户端的回调契约 22 ICallback callback = OperationContext.Current.GetCallbackChannel<ICallback>(); 23 //调用客户端方法 24 callback.DisplayResult(x, y, result); 25 } 26 } 27 }
在上一步,在服务端的 回调契约中 定义的 DisplayResult 方法,但是那仅仅是一个接口,这个接口是由客户端 去实现的,所以服务器端的 只去实现 ICalculator 这个接口就可以了。
**************************************************************************************************************************
再看服务器寄宿
代码:
[ 4-05 ]
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.ServiceModel; 6 using Services; 7 8 namespace Hosting 9 { 10 class Program 11 { 12 static void Main(string[] args) 13 { 14 /*在WCF绑定类型中,WsDualHttpBinding和NetTcpBinding均提供了对双工通信的支持 15 * ,但是两者在对双工通信的实现机制上却有本质的区别。WsDualHttpBinding是基于 16 * HTTP传输协议的;而HTTP协议本身是基于请求--回复的传输协议,基于HTTP的通道本质 17 * 上都是单向的,WsDualHttpBinding实际上创建了两个通道,一个用于客户端向服务端 18 * 的通信,而另一个则用于服务端到客户端的通信,从而间接地提供了双工通信的实现。 19 * WsDualHttpBinding通过创建两个单向信道的方式提供双工通信的实现 20 * NetTcpBinding完全基于支持双工通信的TCP协议. 21 */ 22 using(ServiceHost host = new ServiceHost(typeof(CalculatorService))) 23 { 24 host.Open(); 25 Console.Read(); 26 } 27 } 28 } 29 }
注意:我们这里的服务端 采用的 是 NetTcpBinding(TCP) 方式的 通信,也就是说 当打通 客户端到服务端的通信 就 创建了 两条通信(一条进一条出)。而如果使用的是WsDualHttpBinding ( HTTP ),则只会创建一条通信,进出的消息都在本通信内完成。
配置:
[ 4-06 ]
1 <?xml version="1.0" encoding="utf-8" ?> 2 <configuration> 3 <system.serviceModel> 4 <services> 5 <service name="Services.CalculatorService"> 6 <endpoint address="net.tcp://127.0.0.1:9988/CalculatorService" binding="netTcpBinding" contract="Contracts.ICalculator"></endpoint> 7 </service> 8 </services> 9 </system.serviceModel> 10 </configuration>
可以看到我们的服务器寄宿 没有什么变化
**************************************************************************************************************************
最后看我们的 客户端 :
首先去实现 我们在 契约层定义的 客户端 回调契约 ICallback
[ 4-07 ]
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using Contracts; 6 7 namespace Client 8 { 9 /// <summary> 10 /// 实现回调契约 11 /// 客户端程序为回调契约提供实现。 12 /// </summary> 13 class CalculateCallback:ICallback 14 { 15 public void DisplayResult(double x, double y, double result) 16 { 17 Console.WriteLine("x+y={2} when x={0} and y={1}",x,y,result); 18 } 19 } 20 }
这样我们的回调契约就实现了,可以看出,服务端 的 契约 在 Services层的CalculatorService类去实现,而 客户端的 回调契约 在 客户端 Client 层的CalculateCallback 类去实现。
[ 4-08 ]
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.ServiceModel; 6 using Contracts; 7 8 namespace Client 9 { 10 class Program 11 { 12 static void Main(string[] args) 13 { 14 /* 在服务端调用的程序中,通过DuplexChannelFactory<TChannel>创建服务代理对象 15 * DuplexChannelFactory<TChannel>和ChannelFactory<TChannel>的功能都是一个服务代理对象 16 * 不过DuplexChannelFactory<TChannel>专门用于基于双工通信的服务代理创建 17 * 在创建DuplexChannelFactory<TChannel>之前,先创建回调对象,并通过InstanceContext对回调对象进行包装. 18 */ 19 20 //完成对回调对象的包装 21 InstanceContext instanceContext = new InstanceContext(new CalculateCallback()); 22 //创建基于双工通信的服务代理 23 using (DuplexChannelFactory<ICalculator> channelFactory = new DuplexChannelFactory<ICalculator>(instanceContext, "CalculatorService")) 24 { 25 ICalculator proxy = channelFactory.CreateChannel(); 26 using (proxy as IDisposable) 27 { 28 proxy.Add(1,2); 29 Console.Read(); 30 } 31 } 32 } 33 } 34 }
[ 4-09 ]
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:9988/CalculatorService" binding="netTcpBinding" contract="Contracts.ICalculator"></endpoint> 6 </client> 7 </system.serviceModel> 8 </configuration>
客户端的配置也没有什么变化。
这个Demo 的 整体流程就是:
[ 4-10 ]
我们这个Demo已经定义了 服务契约以及回调契约 都没有返回值(Void) 的 方法
所以 客户端请求 服务端 就没有下文了,而服务端接收到了客户端的 请求 之后 处理结果,再根据客户端的 回调对象 调用客户端的 方法 就没有下文了。
注意 : 这 服务契约 与 回调契约 中 都是 void 并且 特性上打了标签 IsOneWay = true 。
那如果我们将 ICallback 契约中DisplayResult方法上的特性IsOneWay = true 去掉,程序运行 将 Hosting设为 启动项 运行,然后使用 客户端 去调用服务端 :
[ 4-11 ]
这里报错了。 产生了 死锁。 想想,这是为什么?
下一章 说!