WCF z

终结点与服务寄宿

因为最近可能要使用WCF作开发,开始重读蒋金楠的《WCF全面解析》,并整理我的学习WCF的笔记。html

  蒋金楠的书是个人第一本WCF入门书,虽然说硬着头皮啃下来了,可是原理内容太多太多,没有长期的经验是没法掌握的,并且这本书写得太过于偏重原理,并非一本稍微看看就能速成并实现工做需求的书。数据库

  因而这篇文章的目的就是把原理和类结构分析给干掉(毕竟书上都有,我何须抄书?),把一些关键话语和配置方式摘出来,以便往后一旦遇到问题有个速查的手册,也至关于为这本书作了一个索引。编程

  不过不得不说的是,对我这样的新手,读完了书中对WCF类结构的分析,对本身的框架设计能力的确起到了必定促进做用。缓存

  这篇文章算是写给我本身看的吧。本人是WCF新手,文中难免有错误之处,还望老鸟们不吝赐教!安全

 

1、WCF在企业架构中的位置服务器

  WCF是微软对分布式技术的一个整合平台,它对外提供了一套简洁的API,屏蔽了分布式通讯的复杂性。网络

  WCF是微软实现SOA(面向服务架构)的解决方案,何谓服务?我的理解:没有界面、让别人来调的应用程序就是服务。好比说个人网站要显示天气预报,我只要知道天气预报网站给个人接口就能够了,具体实现细节我不用管,这就是SOA的松耦合性。数据结构

  做为客户端也就是服务的消费者,我必需要 知道服务部署在哪,要知道如何通讯,也要知道双方的数据传输规则,在WCF中这三样内容被总体称为“终结点”,上述三个方面被称为“地址、绑定、契约” (也就是“ABC”),也就是说终结点是服务的“出入口”。下面是一副概念图:架构

   

  做为客户端,我既然知道服务如何定义(既 然拿到了服务接口),就能够经过代理模式来封装网络通讯,使服务的真正消费程序依赖接口编程。在WCF当中客户端持有的是一个等效的服务接口,代理类内部 根据终结点的配置(反射xml)自动包办了信道建立等通讯相关逻辑,使得咱们的客户端编程十分简单。并发

  两台机子调服务必然牵扯到数据的传输,通讯数据不能赤裸裸滴暴露在网络上,势必会有一些加解密、压缩、编码等所谓“切面”操做,这一点WCF是经过“绑定”的手段来实现的,我的理解它其实就是一个“建立装饰模式的工厂”。

  数据传输依托于数据结构,数据结构包含了 方法的定义以及传输参数的定义,这二者分别对应WCF的服务契约和数据契约,这两个契约经过反射标签的方式定义,在WCF框架运行时替咱们序列化成咱们制 定的形式,使得咱们的代码依旧十分简洁,不用关注序列化细节,同时WCF还提供了一些契约版本兼容的手段。

  对咱们一线码农来讲,既然WCF编程自己比较简单,那么咱们的难点就是设计一个牛逼的接口了。

 

2、WCF例子

  下面来作一个基于服务的加法计算器——由客户端调用宿主暴露出来的加法服务。

  首先定义服务接口(服务契约),创建一个名为Service.Interface的类库,引用WCF的核心程序集——System.ServiceModel.dll,以后添加以下代码:

 
namespace Service.Interface
{
    [ServiceContract(Name="AddService", Namespace="http://www.xxx.com/")]
    public interface ICalculator
    {
        [OperationContract]
        int Add(int num1, int num2);
    }
}
 

    如上述代码所示,咱们须要显示地把须要暴露出去的服务接口打上服务契约和操做契约的反射标记,其中服务契约当中的Name属性用来决定客户端代理类当中接口的名字(这一接口和ICalculator等价)。

    接下来建立服务实现,新建一个类库Service,引用上面的接口项目,并增长以下代码:

 
namespace Service
{
    public class CalculatorService : ICalculator
    {
        public int Add(int num1, int num2)
        {
            return num1 + num2;
        }
    }
}
 

    在实际应用当中,这个具体Server类应该做为领域层的Facade,而不是充斥着大量业务逻辑代码,另外我的认为应当把接口和服务定义在两个dll里。

    搞定了服务契约和服务实现以后,接下来要把这个服务host到一个宿主里,WCF支持把服务host到IIS里或者一个单独的进程里,这里把加法服务host到一个cmd进程里。因而咱们想固然地创建一个叫Service.Host的cmd项目。

    首先做为服务,必定是和编程语言无关的,那么势必有某种规则来统一不一样语言之间的数据结构差别,WCF服务的WSDL形式描述信息经过“元数据”发布出 来,也就是说咱们要在Host程序里定义一个写代码来定义元数据的发布行为,另外咱们须要定义一个“终结点”来把服务自己给暴露出去。这些代码写起来比较 复杂,不过WCF容许咱们经过写XML的方式来进行配置(又见反射工厂= =!),给咱们省了很大的开发量。

    让Host引用System.ServiceModel.dll以及服务接口还有Service实现项目(IoC的气味浓郁…),实现以下代码:

 
class Program
{
    static void Main(string[] args)
    {
        using (ServiceHost host = new ServiceHost(typeof(CalculatorService)))
        {
            host.Open();
            Console.Read();
        }
    }
}
 

这样就完成了服务端代码,固然,还须要对应的配置文件App.Config:

 
<configuration>
  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior name="metadataBehavior">
          <serviceMetadata httpGetEnabled="true"
                           httpGetUrl="http://127.0.0.1:9527/calculatorservice/metadata" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <services>
      <service name="Service.CalculatorService"
               behaviorConfiguration="metadataBehavior" >
        <endpoint address="http://127.0.0.1:9527/calculatorservice"
                  binding="wsHttpBinding"
                  contract="Service.Interface.ICalculator" />
      </service>
    </services>
  </system.serviceModel>
</configuration>
 

  配置的上半部分定义的一个发布元数据的行为,访问httpGetUrl节当中的地址就能获取服务的元数据。下半部分则是咱们的服务,其中定义了一个终结点,咱们能够看到终结点的ABC三大属性。如今能够运行这个cmd程序来把服务host起来了。

  最后建立客户端,一个cmd项目 Client,引用接口项目,而后右击添加服务引用,输入上面的元数据地址,便可找到这一服务。添加这一服务引用后,项目当中会自动生成一个 app.config文件,咱们改写它(其实能够直接用自动生成的,这里因为引用了接口dll,就重写了一下,实际中不会这么弄):

 
<configuration>
  <system.serviceModel>
    <client>
      <endpoint name="MyServiceEndpoint1"
                address="http://127.0.0.1:9527/calculatorservice"
                binding="wsHttpBinding"
                contract="Service.Interface.ICalculator"  />
    </client>
  </system.serviceModel>
</configuration>
 

  这个客户端的终结点的ABC和服务端的是匹配的,name是终结点的名字,方便咱们在编程时根据名字(反射)调用相关的配置信息,客户端以下:

 
namespace Client
{
    class Program
    {
        static void Main(string[] args)
        {
            using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("MyServiceEndpoint1"))
            {
                ICalculator proxy = channelFactory.CreateChannel();
                Console.WriteLine(proxy.Add(1, 1));
            }
            Console.Read();
        }
    }
}
 

  能够看出,客户端经过配置文件建立了一个 叫信道工厂的东西并“开启”了它(using块),在其中由信道工厂建立了一个服务代理,并经过代理透明地调用服务(疑似有AOP的味道…),至此一个最 简单的WCF服务就开发完成了,程序自己其实简单,其实在信道工厂的背后WCF为咱们实现了不少东西。

  至于如何把WCF服务host到IIS上,网上有很多帖子,这里就不赘述了。 

 

    3、配置终结点地址

    上一步看到的服务端host使用的是XML,其实它的部分代码以下(P9):

 
using (ServiceHost host = new ServiceHost(typeof(CalculatorService)))
{
    host.AddServiceEndpoint(typeof(ICalculator), new WSHttpBinding(),"http://127.0.0.1:9527/calculatorservice");
    if (host.Description.Behaviors.Find<ServiceMetadataBehavior>() == null)
    {
        ServiceMetadataBehavior behavior = new ServiceMetadataBehavior();
        behavior.HttpGetEnabled = true;
        behavior.HttpGetUrl = new Uri("http://127.0.0.1:3721/calculatorservice/metadata");
        host.Description.Behaviors.Add(behavior);
    }
 

  这里的AddServiceEndpoint方法顾名思义就是添加服务终结点,其中有三个参数,分别是服务契约、绑定和地址。

  对于一个服务终结点,能够像上面同样直接指定绝对地址,也能够经过“基地址+绝对地址”的方式配置,就是说,上面Host用的XML能够改为这样:

 
<configuration>
  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior name="metadataBehavior">
          <serviceMetadata httpGetEnabled="true"
                           httpGetUrl="http://127.0.0.1:9527/calculatorservice/metadata" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <services>
      <service name="Service.CalculatorService"
               behaviorConfiguration="metadataBehavior" >
        <host>
          <baseAddresses>
            <add baseAddress="http://127.0.0.1:9527/"/>
          </baseAddresses>
        </host>
        <endpoint address="calculatorservice"
                  binding="wsHttpBinding"
                  contract="Service.Interface.ICalculator" />
      </service>
    </services>
  </system.serviceModel>
</configuration>
 

  一个服务能够配置多个基地址,可是它们的传输协议不能相同,不然会报错(P26)。

    对于host到IIS的状况,svc文件的地址就是服务的基地址,因此不用配置基地址,只要在终结点里配置相对地址(P27)。

    对于实现了多个服务契约的服务(接口分离原则),假定有一个新的接口叫ICalculator2,则须要配置另外一个终结点,这两个终结点实质上公用一个绑定对象(P28)。

 
    <services>
      <service name="Service.CalculatorService"
               behaviorConfiguration="metadataBehavior" >
        <host>
          <baseAddresses>
            <add baseAddress="http://127.0.0.1:9527/"/>
          </baseAddresses>
        </host>
        <endpoint address="calculatorservice"
                  binding="wsHttpBinding"
                  contract="Service.Interface.ICalculator" />
        <endpoint address="calculatorservice"
                  binding="wsHttpBinding"
                  contract="Service.Interface.ICalculator2" />
      </service>
    </services>
 

在客户端一样要配置两个终结点与这两个服务契约相对应:

 
<configuration>
  <system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="MyWSHttpBinding" />
      </wsHttpBinding>
    </bindings>
    <client>
      <endpoint address="http://127.0.0.1:9527/calculatorservice"
                binding="wsHttpBinding" bindingConfiguration="MyWSHttpBinding"
                contract="Service.Interface.ICalculator"
        name="MyServiceEndpoint1">
      </endpoint>
      <endpoint address="http://127.0.0.1:9527/calculatorservice"
                binding="wsHttpBinding" bindingConfiguration="MyWSHttpBinding"
                contract="Service.Interface.ICalculator2"
        name="MyServiceEndpoint2">
      </endpoint>
    </client>
  </system.serviceModel>
</configuration>
 

  仔细一看这个配置会发现contract指向的是接口dll当中的类名,这也致使了客户端要引用这个dll,这就至关恶心了,既然是SOA怎么能引dll呢?因而去掉Client对接口dll的引用,这样一来客户端建立信道工厂的代码就会出错了,缘由是找不到接口,其实在咱们引用WCF服务以后,在客户端会生成一个等效的接口,打开服务引用的客户端代码会Reference.cs发现,这里面含有一个叫AddService的接口,其中包含Add方法,也打着服务标签,这其实就是上面图中所画的“等效契约”,之因此叫AddService,是由于真正的服务契约标签上给他配的Name=”AddService”的缘故,下面有个类AddServiceClient实现了这个接口,这就是咱们的代理类(所谓的“透明代理模式”),行了,这就是我想要的,因而果断改写客户端:

 
static void Main(string[] args)
{
    AddServiceClient addProxy = new AddServiceClient("MyServiceEndpoint1");
    Console.WriteLine(addProxy.Add(1, 1));

    SubServiceClient subProxy = new SubServiceClient("MyServiceEndpoint2");
    Console.WriteLine(subProxy.Sub(10, 5));

    Console.Read();
}
 

客户端配置文件也要修改为等效的契约:

 
    <client>
      <endpoint address="http://127.0.0.1:9527/calculatorservice"
                binding="wsHttpBinding" bindingConfiguration="MyWSHttpBinding"
                contract="ServiceReference.AddService"
        name="MyServiceEndpoint1">
      </endpoint>
      <endpoint address="http://127.0.0.1:9527/calculatorservice"
                binding="wsHttpBinding" bindingConfiguration="MyWSHttpBinding"
                contract="ServiceReference.SubService"
        name="MyServiceEndpoint2">
      </endpoint>
    </client>
 

这样Client就摆脱对接口dll的引用了(P30)。

下面利用地址报头进行辅助寻址,WCF通讯是创建在消息交换基础之上的,一个完整的SOAP消息应该包含报头和主体,报头用于添加一些额外的控制信息,比方说在服务器端终结点增长一个报头:

 
        <endpoint address="calculatorservice"
                  binding="wsHttpBinding"
                  contract="Service.Interface.ICalculator">
          <headers>
            <sn xmlns="http://www.xxx.com/">{DDA095DA-93CA-49EF-BE01-EF5B47179FD0}</sn>
          </headers>
        </endpoint>
 

这样一来客户端调用就会抛异常了,缘由是没法找到匹配的终结点,因此须要在客户端加上一样的报头才能让终结点匹配(P40)。

报头能够是一个序列化后的对象,能够经过代码实现报头的添加(P40)。

如但愿屏蔽掉报头对寻址的影响,能够给服务实现类打上标签:

[ServiceBehavior(AddressFilterMode = AddressFilterMode.Any)]
public class CalculatorService : ICalculator, ICalculator2

  这个特性用来改变针对终结点的筛选机制,默认的枚举值是Exact精确匹配,改成Any则是任意匹配,另外一个枚举值是Prefix表示基于前缀匹配(P41)。

 

  4、端口共享与监听地址配置

  将某个WCF服务host到一个进程上, 本质就是经过这个进程来监听Socket请求(P43),而对于防火墙,一般只保留80/443端口,而前面的WCF例子当中若是把两个服务都经过一个端 口暴露就会抛异常(P42),因此首先,咱们要在控制面板-管理工具-服务当中启NET.TCP Port共享服务,并在WCF服务中使用TCP协议,此外要本身增长一个绑定:

 
<configuration>
  <system.serviceModel>
    <bindings>
      <netTcpBinding>
        <binding name="MyBinding1" portSharingEnabled="true" />
      </netTcpBinding>
</bindings>
 

并修改终结点的绑定:

<endpoint address="calculatorservice"
binding="netTcpBinding" bindingConfiguration="MySharingBinding1"
contract="Service.Interface.ICalculator">

  有时候出于负载均衡考虑,消息的监听地址和实际服务地址并非同样的,好比用端口9999的地址来监听请求,以后转发给端口8888的地址来实现,那么在配置服务的时候就须要在终结点上增长一个监听地址:

<endpoint address="http://127.0.0.1:8888/calculatorservice" listenUri="http://127.0.0.1:9999/calculatorservice" 
listenUriMode="Explicit"
binding="netTcpBinding" bindingConfiguration="MySharingBinding1"
contract="Service.Interface.ICalculator">

  这里的listenUriMode属性有两个值,一个是Explicit,意思是你怎么配的,就怎么调用,另外一个是Unique会采用不一样的策略来保证监听地址惟一(P49)。

 

5、信道与绑定

  在WCF应用层的下方存在一系列首位相连 的信道,它们组成了信道栈(十有八九是装饰模式或者职责链之类的东西),咱们经过代理类调用一个服务方法,调用进入信道栈,在这些信道栈上首先处理消息编 码、安全传输等功能(P66),通过这些信道处理后的消息会以某种方式进行序列化,并进行传输,到达接收端后也会反过来执行这些信道来恢复消息(因此两边 的配置要一致)。

  与纯Socket相似,在WCF当中,消息收发的过程是:建立绑定对象,建立信道管理器(监听器/工厂),开始监听,并在监听线程的循环内收发消息(P68)。

  信道分为三类:传输信道、编码信道、协议信道。WCF的绑定是讲究顺序的,由于绑定的顺序决定信道管理器的顺序,进而决定信道顺序(P72)。

  WCF的信道由信道工厂(客户端的叫法)/监听器(服务端的叫法)建立,信道工厂/监听器由绑定元素建立,这三者都是能够经过咱们手写代码进行二次开发的(P83-P96)。

  绑定是绑定元素的有序集合,若是想肯定某绑定是否具备某功能,只要看有没有对应的绑定元素就能够了(P105)。绑定也是能够由咱们自定义的(P99),二次开发的自定义绑定也能够经过配置文件来调用(P110),关于配置一个自定义绑定能够见这篇文章(http://www.cnblogs.com/leslies2/archive/2011/10/14/2195800.html)。

  WCF提供了三种消息交换模式:数据报模式,发出消息后不但愿对方回复。这样的发送通常采用异步方式;请求回复模式,通常采用同步调用;双工模式,任何一方均可以给对方发送消息,可让服务端回调客户端方法(P76)。

  WCF同时为咱们提供了一些系统自带的绑定,这些绑定大多能解决咱们的需求,上面的帖子里也已经总结了一个列表。其中Net开头的绑定限制在.Net平台使用,这些绑定适用于内网通讯,以WS开头的绑定适合跨平台通讯等(P105)。

  我的理解,自定义绑定是WCF提供的一种扩展机制,这些东西一旦用上只能查书看帖子来配置,因此贴代码是没意义的。

  不得不提的是,其中经过链状的工厂生成链状的“方法切面”这个思路是很值得学习的!

 

6、服务契约与操做契约

  接口提取的是变更中的“不变”部分,同 样,契约则定义了服务对外的“承诺”,即消息交互的“结构”。做为服务的消费者,咱们须要知晓服务的契约,按照契约规定的方式调用和传参,才能获得预期的 结果。调用WCF的方式神似于“针对接口编程”,由于服务是“自治”的,二者依赖于“契约”,而无需知道服务的实现细节。(P115)

  WCF定义了服务契约和数据契约,若是把 一个服务看作是一个程序集的元数据的话,则前者定义了期中的方法,后者定义了其中的属性(数据结构)。服务是经过WSDL(Web服务描述语言)定义的, 它实质上是个xml结构,经过它实现了跨语言,以知足混搭系统的须要。(P116)

  如前面例子看到,定义务契约要用到两个标签[ServiceContract]和[OperationContract],前者贴在接口的定义上,后者贴在方法的定义上。

  [ServiceContract]服务契约标签有Name和NameSpace等不少属性,它们对应wsdl中某些节点的值。

  Name属性默认是接口名,可是咱们能够 修改,经过这个配置能够给服务起个新名字,其中Name属性能改变客户端生成的代理类的名字,为“Name的属性值+Client”。NameSpace 属性建议使用公司或者项目的名称。ConfigurationName属性用于指定服务契约在终结点中的配置名称,经过它咱们就不用在xml终结点的契约 配置里配一对类名了,只要配上这个ConfigurationName的值便可,脱离的xml和具体类名的耦合。若是不配,默认就是接口名。

  [OperationContract]操做契约标签一样也有不少属性,操做契约是对操做的描述,WCF使用它的目的是把这些操做转化成某种格式的消息以进行消息交换。

  Name属性表示操做的惟一名称,默认使 用的就是方法名,须要注意的是这个名称不能重复,不然会抛异常,因此最好不要在契约里重载方法。前面曾经提到过能够在终结点内添加报头 (header),WCF在进行消息筛选的时候须要根据请求消息的Action报头决定目标终结点,在执行操做的时候也要根据这个值选择操做,因此这个 Action值也是惟一不能重复的(P126)。Action属性的默认值是“服务契约命名空间/服务契约名称/操做名称”,它对应请求消息,回复消息则 对应一个RelpyAction属性,它的值默认是“服务契约命名空间/服务契约名称/操做名称Responst”。我的感受这个值不用开发人员来动。

  在WCF当中,契约是扁平的结构,契约所依托的接口能够继承,接口的之接口也能够是个操做契约,可是虽然有两个接口,整个服务契约倒是一个(P126)。

  既然服务是要映射成“元数据”的,那么确定就有“反射”这一说,WCF当中对服务的“反射”是经过以下方法实现的:

ContractDescription cd = ContractDescription.GetContract(typeof(客户端代理类名));

  经过这个方法咱们能拿到契约的描述信息, 这其中又有一个Operations集合属性,描述这个契约里的全部操做。而这个Operations集合的每一个operation当中又有一个 Messages集合,它描述请求/响应的消息,能够经过这个消息分析报文(P136)。

 

    7、客户端代理类

  最后来回顾一下客户端,假定咱们给服务起 的名字叫MyCalculatorService,其产生的客户端类为MyCalculatorServiceClient,对其F12,能够发现它继承 自一个泛型的ClientBase<TChannel>,以及实现了一个接口,这个接口就是所谓的“等效接口”:

 
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
[System.ServiceModel.ServiceContractAttribute(Namespace="http://www.xxx.cn/", ConfigurationName="ServiceReference.MyCalculatorService")]
public interface MyCalculatorService {
    [System.ServiceModel.OperationContractAttribute(Action="http://www.xxx.cn/MyCalculatorService/Add", ReplyAction="http://www.xxx.cn/MyCalculatorService/AddResponse")]
    int Add(int num1, int num2);
}
 

  能够看出,这里的接口和方法上也贴了标 签,说明WCF的确是根据服务端代码上的反射标签生成的wsdl内容,客户端对服务的wsdl进行分析,并产生了这个接口,而客户端 MyCalculatorServiceClient类中,除了拥有一些构造函数,剩余部分就是对这个接口的实现。全部的实现都是经过 “base.Channel.方法名”调用的,这个父类就是ClientBase<TChannel>,它其中包含一个Channel属性:

// 摘要:
//     获取用于将消息发送到不一样配置的服务终结点的内部通道。
//
// 返回结果:
//     指定类型的通道。
protected TChannel Channel { get; }

这里的TChannel类型就是信道的类型。

  显然客户端使用的并非服务器端的接口,这种“透明代理模式”正符合最上面那幅图当中的结构。

消息交换、服务实例、会话与并发

8、消息交换模式

  WCF服务的实现是基于消息交换的,消息交换模式一共有三种:请求回复模式、单向模式与双工模式。

  请求回复模式很好理解,好比int Add(int num1, int num2)这种方法定义就是典型的请求回复模式,请求者发送两个数字,服务回复一个结果数字。若是采用ref或者out参数,那么在xsd当中,ref参 数会做为输入和输出参数,out参数只做为输出参数。在WCF当中void返回值的操做契约其实也是请求响应模式的,由于将返回值改成void,影响的只 是回复消息的xsd结构,void返回的是一个空xml元素(P141)。

  对于一些调用服务记录日志等不要求有响应(即使抛异常也不须要客户端知道)的行为,应该采用单向模式,单向模式只须要在操做契约上添加单向的属性:

[OperationContract(IsOneWay=true] void WriteLog(string msg);

  单向模式的操做在对应的wsdl当中没有输出节点,这样的操做必须使用void做为返回值,其参数也不可以使用ref和out参数(P144)。

  最后一类是双工模式,双工模式是在服务端定义接口,由客户端实现这个方法,服务端“回调”客户端的这个方法。这里直接扒书加法的例子,由于这个例子又简单又能说明问题,这个例子当中客户端调用服务端的加法,服务端回调客户端的显示函数。

  首先定义服务契约:

[ServiceContract(Namespace = "http://www.artech.com/", CallbackContract = typeof(ICalculatorCallback))] public interface ICalculator { [OperationContract(IsOneWay = true)] void Add(double x, double y); }

这里定义了CallbackContract属性,须要传入一个接口的名字,这个接口名字就是回调操做契约,既然在这里指明了它是个契约,就无需服务契约标签了,这里之因此采用单向,是为了防止死锁:

public interface ICalculatorCallback { [OperationContract(IsOneWay = true)] void DisplayResult(double result, double x, double y); }

契约实现以下:

复制代码
public class CalculatorService : ICalculator { public void Add(double x, double y) { double result = x + y; ICalculatorCallback callback = OperationContext.Current.GetCallbackChannel<ICalculatorCallback>(); callback.DisplayResult(result, x, y); } }
复制代码

注意实现的第二行,先从当前操做上下文当中拿到了回调信道,以后调用它的回调方法。

客户端实现以下:

复制代码
public class CalculatorService : ICalculator { public void Add(double x, double y) { double result = x + y; ICalculatorCallback callback = OperationContext.Current.GetCallbackChannel<ICalculatorCallback>(); callback.DisplayResult(result, x, y); } }
复制代码

首先是一个回调函数的实现类,它实现了回调契约,不过老A的例子有些不雅,这里直接引了契约的dll。

而后是客户端的主体:

复制代码
class Program { static void Main(string[] args) { InstanceContext callback = new InstanceContext(new CalculatorCallbackService()); using (DuplexChannelFactory<ICalculator> channelFactory = new DuplexChannelFactory<ICalculator>(callback, "calculatorservice")) { ICalculator calculator = channelFactory.CreateChannel(); calculator.Add(1, 2); } Console.Read(); } }
复制代码

这里首先建立了实例上下文,用它和终结点的配置一块儿建立了双工信道工厂,以后经过这个工厂建立信道来实现双工调用(这里不雅同上)。

  服务端的配置以下:

复制代码
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.serviceModel> <behaviors> <serviceBehaviors> <behavior name="exposeExceptionDetail"> <serviceDebug includeExceptionDetailInFaults="true"/> </behavior> </serviceBehaviors> </behaviors> <services> <service name="Artech.WcfServices.Service.CalculatorService" behaviorConfiguration="exposeExceptionDetail"> <endpoint address="http://127.0.0.1:3721/calculatorservice" binding="wsDualHttpBinding" contract="Artech.WcfServices.Service.Interface.ICalculator"/> </service> </services> </system.serviceModel> </configuration>
复制代码

这里采用了支持双工通讯的wsDualHttpBinding绑定,客户端配置以下:

复制代码
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.serviceModel> <client> <endpoint name ="calculatorservice" address="http://127.0.0.1:3721/calculatorservice" binding="wsDualHttpBinding" contract="Artech.WcfServices.Service.Interface.ICalculator"/> </client> </system.serviceModel> </configuration>
复制代码

 

  9、实例与会话

  上面了例子里有一个InstanceContext对象, 这个对象就是实例上下文,它是对服务实例的封装,对于一个调用服务的请求,WCF会 首先反射服务类型来建立服务实例,并用实例上下文对其进行封装(固然这个实例是带“缓存”的),咱们能够配置必定的规则来释放上下文(P396)。

  实例上下文分为三种模式:单调模式、会话模式和单例模式。上下文的模式是服务的行为,与客户端无关,以[ServiceBehavior]的InstanceContextMode属性来设置。下面分别来看一看这三种模式。

  单调模式,表示每一次调用服务都会建立一个全新的服务实例和上下文,上下文的生命周期与服务调用自己绑定在一块儿(P402),这种方式能最大限度地发挥资源利用率,避免了资源的闲置和竞争,所以单调模式适合处理大量并发的客户端(P406)。

  实现单调模式须要在服务的实现类上增长反射标记:

[ServiceBehavior(InstanceContextMode=InstanceContextMode.PerCall)]
public class CalculatorService : ICalculator

  从这里也能看出,服务的实现类并不表明业务逻辑,而是位于业务逻辑之上的一个“隔离层”,它显然属于服务层。

  单例模式则走了另外一个极端,这种模式让整个服务器上自始至终只存在一个上下文,它的反射标签是:

[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]

  既然只有一个上下文,那么说明同时只能处理一个请求,剩下的请求去排队或者超时。这种模式只能应付不多的客户端,并且仅限于作全局计数这样的操做。若是须要让这个服务异步执行,须要这样写反射标签:

[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single,ConcurrencyMode=ConcurrencyMode.Multiple)]

  会话模式则将为每个服务代理生成一个上下文,会话使服务具备识别客户端的能力,因此必定要选用支持会话的绑定(P420),这种模式适合于客户端数量不多的应用。

  这种模式的服务契约上面有SessionMode标签,Required对服务的整个调用必须是一个会话,默认值为Allowed,会在适当时 机采用会话模式。服务契约含有IsInitiating和IsTerminating两个属性,在客户端调用服务时,必须先调用IsInitiating 为true和IsTerminating为false的,做为起始,最终要调用IsInitiating为false而IsTerminating为 true的,做为终结,在二者之间能够调用全为false的操做。若是不这样调用会报错。

复制代码
[ServiceContract(SessionMode=SessionMode.Required)]
public interface ICalculator { [OperationContract(IsInitiating=true, IsTerminating=false)] void Reset(); [OperationContract(IsInitiating = false, IsTerminating = false)] void Add(int num); [OperationContract(IsInitiating = false, IsTerminating = true)] int GetResult(); }
复制代码

  服务实现以下,首先服务行为加上了InstanceContextMode=InstanceContextMode.PerSession,并在服务的内部保存了一个叫作result的非静态变量:

复制代码
[ServiceBehavior(InstanceContextMode=InstanceContextMode.PerSession)]
public class CalculatorService : ICalculator { private int result; public void Reset() { result = 0; } public void Add(int num) { result += num; } public int GetResult() { return result; } }
复制代码

  上面一共提到了InstanceContextMode和SessionMode两个枚举,当采用PerCall单调服务时,不论 SessionMode如何,中间结果都不会被保存;采起Single单例服务时,不论SessionMode如何中间结果都会被保存,由于上下文是单例 的;采起PerSession会话服务时,只有会话模式为Required和Allowed时,中间结果才会被保存。(P427)一张图说明问题:

  

  

  10、并发

  服务行为的InstanceContextMode表示的是对于一个请求,在服务端搞出几个实例上下文来, 那么,ConcurrencyMode则表示同一个服务实例如何同时处理多个并行到来的请求,这些请求可能来自同一个服务代理的并行调用,也可能来自多个 服务代理的同时调用。

  不过在使用ConcurrencyMode以前,须要先给服务/回调服务加上以下标记:

[ServiceBehavior(UseSynchronizationContext=false)] [CallbackBehavior(UseSynchronizationContext=false)]

  这是由于服务操做会自动绑定服务的寄宿线程,为了打破这种线程的亲和性须要禁用同步上下文,不然服务就将是串行执行的,而且是采用同一个线程执行的,就没有什么“并发”可言了。(下P197)

  对于并发模式,WCF一样提供了三个可选模式。

  Single模式表示一个实例上下文在某时刻只能处理单一请求,也就是说针对某个服务上下文的并发请求会串行执行。

  在这种模式下,当并发请求到来时,WCF会对实力上下文进行上锁。

  Multiple模式表示一个实力上下文能够同时处理多个请求。

  Reentrant(可重入)模式和Single相似,只能同时处理一个请求,然而一旦这个请求处理着一半 就去回调客户端了,那么在客户端响应以前,其余的并行请求仍是能够被它处理的。举个不雅的例子,男人和老婆亲热着一半,老婆出去拿东西了,这时在外排队的 小三就能够进来,等老婆回来了,须要先等小三出来,本身再进去……

  在这种模式下,若是须要服务端对客户端进行回调,那么要么采用OneWay的形式回调,要么就要把服务的并发模式设置为非Single,不然会形成死锁的异常,由于“小三”是会占有“原配”的锁的。(下P182)

  要让服务支持并发,须要给服务打上服务行为标签,默认值是Single,一样也能够给CallbackBehavior标签设置并发模式:

[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Single)]

  一样,前面提到的实力上下文模式和并发模式也是有3*3=9种组合的。

  对于单调模式(PerCall),因为每一个服务调用都使用一个实例上下文,因此根本不存在并发状况,无需设置并发模式,可是对于同一个服务代理,若是须要并行发送请求,则须要手动开启服务代理,不然服务是会串行调用的(P189)。

  对于会话模式(PerSession),并发将按照ConcurrencyMode所配置的方式进行处理。

  对于单例模式(Single),不论并发请求来自一个仍是多个客户端,若ConcurrencyMode是Single则串行,是Multiple则并行,对Reentrant在回调发生时也是并行的(下P195)。

 

  11、限流

   为了防止请求数量过多致使服务器资源耗尽,须要在消息接收和处理系统之间创建一道闸门来限制流量,能够经过服务器端配置给服务添加行为来进行流量控制:

<behavior name="throttlingBehavior"> <serviceThrottling maxConcurrentCalls="16" maxConcurrentInstances="116" maxConcurrentSessions="100"/> </behavior>

  三个属性分别为能处理的最大并发消息数量、服务实例上下文最大数量和最大并发会话数量,1六、11六、100分别是它们的默认值,在WCF4.0后,这些值是针对单个CPU而言的(下P204)。

数据契约、消息契约与错误契约

12、数据契约

  在实际应用当中数据不可能仅仅是以int Add(int num1, int num2)这种简单的几个int的方式进行传输的,而是要封装成相对复杂的Request/Response对象,即用咱们自定义的类来进行消息的传输, 那么就须要一种规则来序列化/反序列化咱们本身的对象成为某种标准格式。WCF能够经过数据契约来完成这一过程,WCF使用的序列化器是 DataContractSerializer。

  在一个类上打上DataContract标记表示这是一个数据契约,其中打上DataMember的属性会被WCF序列化,与是否public无关(P174),例子:

复制代码
[DataContract]
public class Customer { [DataMember] public string Name { get; set; } [DataMember] public string Phone { get; set; } [DataMember] public Address CompanyAddress { get; set; } [DataMember] public Address ShipAddress { get; set; } } [DataContract] public class Address { [DataMember] public string Province { get; set; } [DataMember] public string City { get; set; } [DataMember] public string District { get; set; } [DataMember] public string Road { get; set; } }
复制代码

  DataContract有三个属性,其中Name和NameSpace表示名称和命名空间,IsReference表示若是设置为true, 则在序列化XML的过程中,若是遇到了两个对象使用同一个对象的引用,则只序列化一份这个对象,默认为false(P181)。

  DataMember有四个属性,Name为序列化后在XML中的节点名称,Order为在XML中的排序,默认为-1,从小到大排序,在咱们 队序列化后的结果不满意时能够经过这个属性进行修改,序列化后的数据规则是:父类在前之类在后,同一类型中的成员按照字母排序,IsRequired表示 属性成员是不是必须成员,默认为false可缺省的,EmitDefaultValue表示该值等于默认值时是否序列化,默认为true。

  在应用当中服务可能来回传递很大的DataSet,致使服务器端序列化不堪重负,因而能够修改WCF服务行为的 maxItemInObjectGraph的值来控制最大序列化对象的数量上限,好比设置为2147483647(P178)。如何设置服务行为这里再也不 赘述,能够看个人上一篇笔记。

  SOAP消息里的内容是使用DataContractSerializer序列化的,固然,若是想换一种序列化方式,能够在服务契约类上打标签好比[XmlSerializerFormat]。

  

  十3、继承关系的序列化

  依旧是老A的例子,假设有以下的数据契约和服务:

复制代码
public interface IOrder { Guid Id { get; set; } DateTime Date { get; set; } string Customer { get; set; } string ShipAddress { get; set; } } [DataContract] public abstract class OrderBase : IOrder { [DataMember] public Guid Id { get; set; } [DataMember] public DateTime Date { get; set; } [DataMember] public string Customer { get; set; } [DataMember] public string ShipAddress { get; set; } } [DataContract] public class Order : OrderBase { [DataMember] public double TotalPrice { get; set; } } [ServiceContract] public interface IOrderService { [OperationContract] void ProcessOrder(IOrder order); }
复制代码

在这里数据契约存在继承关系且实现了一个接口,服务契约须要传入一个接口类型做为参数,那么元数据发布后,在客户端就会获得以下的方法:

public void ProcessOrder(object order) { base.Channel.ProcessOrder(order); }

其类型变成了object,这就会形成危险,因此说不推荐在服务操做中使用接口类型做为参数。通过我的实践证实,即使用 ServiceKnownType属性,到了客户端也是一个object类型参数。形成这一现象的缘由就是WCF不知道如何序列化服务契约当中的 IOrder,它不知道这表明了什么,因而序列化到XML时这个数据类型对应的节点就是<anyType>。

  一个恰当的改法就是利用已知类型,修改服务契约,让他使用父类而不是接口,而且修改数据契约,给父类设置之类的已知类型:

复制代码
[ServiceContract]
public interface IOrderService { [OperationContract] void ProcessOrder(OrderBase order); } [DataContract] [KnownType(typeof(Order))] public abstract class OrderBase : IOrder { [DataMember] public Guid Id { get; set; } [DataMember] public DateTime Date { get; set; } [DataMember] public string Customer { get; set; } [DataMember] public string ShipAddress { get; set; } }
复制代码

如此一来,到客户端参数就成为了OrderBase类型,正如咱们所愿的。

  另外一套解决方案是数据契约不变,把针对已知类型的配置放在操做契约上,一样操做契约不能使用接口,以下:

复制代码
[ServiceContract]
[ServiceKnownType("GetKnownTypes", typeof(KnownTypeResolver))] public interface IOrderService { [OperationContract] void ProcessOrder(OrderBase order); } public static class KnownTypeResolver { public static IEnumerable<Type> GetKnownTypes(ICustomAttributeProvider provider) { yield return typeof(Order); } }
复制代码

这里经过一个类来反射获取已知类型。

 

  十4、数据契约的版本控制

  不论服务端仍是客户端,他们的之间发送的数据都是要序列化为XML的,序列化的依据就是XSD,若是双方要保持正常通讯,那么这个XSD就必须等效,这个“等效”指的是契约命名空间和各属性的名称及顺序都必须一致。

  然而程序并非一成不变的,随着需求变化,咱们可能会在服务端的数据契约当中增删一些字段,而没有更新服务引用的客户端在和新版本的服务交互时就会发生问题,对于这种版本不一致形成的问题,WCF提供了解决方案。

  第一种状况是服务端增长了一个字段,而客户端依然经过老版本的数据契约进行服务调用,如此一来在服务端反序列化时就会发现缺乏字段,在这种状况 下,对于缺乏的字段,服务端会自动采用默认值来填充(P210),若是但愿客户端不更新服务则调用错误的话,就须要加上表示数据成员是必须传入的反射标记 了:

[DataMember(IsRequired=true)] public string Description { get; set; }

  若是但愿不实用默认值,而实用我么自定义的值,则须要在数据契约内增长方法:

复制代码
[DataContract]
public abstract class OrderBase : IOrder { [DataMember] public Guid Id { get; set; } [DataMember] public DateTime Date { get; set; } [DataMember] public string Customer { get; set; } [DataMember] public string ShipAddress { get; set; } [DataMember] public string Description { get; set; } [OnDeserializing] void OnDeserializing(StreamingContext context) { this.Description = "NoDescription"; } }
复制代码

  和OnDeserializing相似的还有OnDeserialized、OnSerializing,OnSerialized几个标签,能够在其中增长序列化先后事件。

  第二种状况是服务端减小了一个字段,在这种状况下采用新版本数据契约的服务端在会发给采用老版本数据契约的客户端时就会出现数据丢失的状况。

  在这种状况下,须要给数据契约实现IExtensibleDataObject接口,并注入ExtensionDataObject类型的ExtensionData属性:

复制代码
[DataContract]
public abstract class OrderBase : IOrder, IExtensibleDataObject { [DataMember] public Guid Id { get; set; } [DataMember] public DateTime Date { get; set; } [DataMember] public string Customer { get; set; } [DataMember] public string ShipAddress { get; set; } public ExtensionDataObject ExtensionData { get; set; } }
复制代码

有了这个属性,在序列化的时候就会自动带上额外的属性了,固然,若是但愿屏蔽掉这个功能,则须要在服务行为和终结点行为当中进行配置:

<dataContractSerializer ignoreExtensionDataObject="true" />

 

  十5、消息契约

  其实利用数据契约已经可以很好地完成数据的传输了,而数据契约只能控制消息体,有时候咱们想在数据传递过程 中添加一些额外信息,而不但愿添加额外的契约字段,那么咱们就得改消息报头,也就是说该使用消息契约了。读老A的书,这章的确让我犯晕,上来全是原理,其 中从第232页到第260页的原理已经给我这个初学SOA的新手扯晕了,看来之后讲东西千万不能上来就扯原理啊!既然根本记不住,那么就直接来写代码吧!

  首先改了上面例子里的数据契约,换成消息契约:

复制代码
[MessageContract]
public class Order { [MessageHeader] public SoapHeader header; [MessageBodyMember] public SoapBody body; } [DataContract] public class SoapHeader { [DataMember] public Guid Id { get; set; } } [DataContract] public class SoapBody { [DataMember] public DateTime Date { get; set; } [DataMember] public string Customer { get; set; } [DataMember] public string ShipAddress { get; set; } [DataMember] public double TotalPrice { get; set; } }
复制代码

  消息契约是用MessageContract标签修饰的,咱们要控制的消息头用MessageHeader修饰,消息体则由MessageBodyMember修饰,这样一来就把消息头和消息体拆分开来能够独立变化了。而后,修改服务契约:

[ServiceContract]
public interface IOrderService { [OperationContract] void ProcessOrder(Order order); }

这里将方法的参数设置为了消息契约的对象,须要注意的是若是使用消息契约,则参数只能传一个消息契约对象,不能使用多个,也不能和数据契约混用。

  接下来发布服务,在客户端编写以下代码:

复制代码
static void Main(string[] args) { OrderServiceClient proxy = new OrderServiceClient(); SoapHeader header = new SoapHeader(); header.Id = Guid.NewGuid(); SoapBody body = new SoapBody(); //body.Date = DateTime.Now; //body.……  proxy.ProcessOrder(header, body); Console.ReadKey(); }
复制代码

  消息契约第一个典型应用就是在执行文件传输时,文件的二进制信息放到body里,而一些复加的文件信息则放在head里。

  写完代码以后来看看这些标签的属性。MessageContract标签的IsWrapped属性表示是否将消息主体整个包装在一个根节点下 (默认为true),WrapperName和WrapperNamespace则表示这个根节点的名称和命名空间。ProtectionLevel属性 控制是否对消息加密或签名。

  MessageHeader有一个MustUnderstand属性,设定消息接收方是否必须理解这个消息头,若是没法解释,则会引起异常,这个值能够用来作消息契约的版本控制。

  MessageBody当中有一个Order顺序属性,它不存在于MessageHeader当中,是由于报头是与次序无关的。

  

  十6、消息编码

  SOAP当中的XML是通过编码后发送出去的,WCF支持文本、二进制和MTOM三种编码方式,分别对应 XmlUTF8TextWriter/XmlUTF8TextReader、XmlBinaryWriter/XmlBinaryReader和 XmlMtomWriter/XmlMtomReader。

  选择哪种编码方式取决于咱们的绑定,UTF8编码没什么好解释的,BasicHttpBinding、WSHtpBinding/WS2007HttpBinding和WSDualHttpBinding在默认状况下都使用这种编码。

  若是XML很大,则应该使用二进制的形式,二进制编码会将XML内容压缩传输。NetTcpBinding、NetNamedPipeBinding和NetMsmqBinding都采用这种编码。

  对于传输文件这样的大规模二进制传输场合,应该采用MTOM模式。

  若是咱们须要本身改写编码的方式就须要改绑定的XML或者手写绑定了(P285)。

 

   十7、异常与错误契约

  接下来换另外一个话题——异常处理。和普通服务器编程同样,在WCF的服务端也是会引起异常的,好比在服务器端除了一个0,这时候异常会抛出到服务器端,那么既然WCF是分布式通讯框架,就须要把异常信息发送给调用它的客户端。

  若是把异常的堆栈信息直接发送给客户端,显然是很是危险的(不解释),因此通过WCF的内部处理,只会在客户端抛出“因为内部错误,服务器没法处理该请求”的异常信息。

  若是确实须要把异常信息传递给客户端,则有两种方式,一种是在配置文件里将serviceDebug行为的 includeExceptionDetailInFaulte设置为true,另外一种手段就是在操做契约上增长 IncludeExceptionDetailInFaulte=true的服务行为反射标签,具体代码与前面相似。

  在这种设置之下,抛出的异常的类型为FaultException<TDetail>,这是个泛型类,TDetail在没有指定的状况下是ExceptionDetail类,因而在客户端咱们能够如此捕获异常:

复制代码
CalculatorServiceClient proxy = new CalculatorServiceClient(); int result; try { result = proxy.Div(10, 0); Console.WriteLine(result); } catch (FaultException<ExceptionDetail> ex) { Console.WriteLine(ex.Detail.Message); (proxy as ICommunicationObject).Abort(); }
复制代码

在处理异常以后,须要手动关掉服务代理。

  固然,能够事先在服务器端定义好一些异常,用来直接在客户端来捕获非泛型的异常:

复制代码
public int Div(int num1, int num2) { if (num2 == 0) { throw new FaultException("被除数不能为0!"); } return num1 / num2; }
复制代码
复制代码
CalculatorServiceClient proxy = new CalculatorServiceClient(); int result; try { result = proxy.Div(10, 0); Console.WriteLine(result); } catch (FaultException ex) { Console.WriteLine(ex.Message); (proxy as ICommunicationObject).Abort(); }
复制代码

  可是从习惯上来说咱们喜欢把异常封装成一个含有其余信息的对象序列化返回给客户端,显而易见这个对象必定要是一个数据契约,首先定义一个数据契约来记录出错的方法和消息:

复制代码
[DataContract]
public class CalculatorError { public CalculatorError(string operation, string message) { this.Operation = operation; this.Message = message; } [DataMember] public string Operation { get; set; } [DataMember] public string Message { get; set; } }
复制代码

以后在服务端抛出,这里的泛型类就是承载错误的数据契约的类型:

if (num2 == 0) { CalculatorError error = new CalculatorError("Div", "被除数不能为0!"); throw new FaultException<CalculatorError>(error, error.Message); }

如此作还不够,还须要给会抛出这种异常的操做加上“错误契约”:

复制代码
[ServiceContract]
public interface ICalculatorService { [OperationContract] [FaultContract(typeof(CalculatorError))] int Div(int num1, int num2); }
复制代码

如此就能在客户端捕获具体泛型类的错误了:

复制代码
try { result = proxy.Div(10, 0); Console.WriteLine(result); } catch (FaultException<CalculatorError> ex) { Console.WriteLine(ex.Detail.Operation); Console.WriteLine(ex.Detail.Message); (proxy as ICommunicationObject).Abort(); }
复制代码

  须要注意的是,一个操做能够打多个错误契约标记,可是这些错误契约的名称+命名空间是不能重复的,由于自定义的错误类型会以WSDL元数据发布出去,若是有重复的名称,就会发生错误(下P17)。

  同时,WCF也支持经过标签方式将错误类采用XML序列化,这里再也不赘述(下P18)。

事务编程与可靠会话

真不愧是老A的书,例子多,并且也讲了很多原理方面的内容,不过越读越以为压力山大……此次来稍微整理整理事务和可靠会话的内容。

  

  十8、事务编程

  WCF的分布式事务编程,指的是在客户端定义一个事务范围,在这个范围内对WCF服务进行连续调用,能够实现其中一个出现问题总体回滚的效果。因为WCF依赖于MSDTC,因此首先须要开启MSDTC服务才可以经过WCF进行分布式事务编程。

  这里我也本身写了一个典型的银行转帐的练习,首先须要创建一个数据库,数据表只有一张Account表,其中有AccountId和Money两个int型字段,AccountId为主键。里面有两个帐户:帐户1有1000,帐户2有1000。

  首先,既然是分布式事务,事务须要从客户端流转到服务端,那么它们之间就应该达成“共识”,也就是说须要对服务契约作手脚。下面定义一个服务契约:

复制代码
[ServiceContract(SessionMode = SessionMode.Required)]
public interface IBankService { [OperationContract] [TransactionFlow(TransactionFlowOption.Mandatory)] void OutMoney(int fromAccountId, int money); [OperationContract] [TransactionFlow(TransactionFlowOption.Mandatory)] void InMoney(int toAccountId, int money); }
复制代码

  在转帐的整个过程中,用户首先发送一个OutMoney请求,减小帐户1当中的钱,以后发送InMoney请求,增长帐户2的钱。显然这个契 约须要一个会话,因此给服务契约增长SessionMode属性。注意,在实际应用当中转帐应该做为一个完整的服务而不是两个服务方法,这里只是举个例子 而已。

  事务的流转是一个操做行为,因此须要在操做契约上增长TransactionFlow标记,并设置TransactionFlowOption 的值,这个标记是事务的总开关。其值有三个:NotAllowed(默认值,客户端事务禁止经过该方法流入服务端),Allowed(容许流入事 务),Mandatory(必须在事务内调用),这里将转帐操做设置为必须在事务内。

  因为事务操做必然伴随着消息交换,因此OneWay操做必然是不支持事务的,即OneWay操做的TransactionFlowOption只能为NotAllowed(下P143)!

  接下来实现这个服务操做,代码以下,其中Repository细节就不贴了:

复制代码
[ServiceBehavior(TransactionIsolationLevel = IsolationLevel.Serializable, TransactionTimeout = "00:05:00", TransactionAutoCompleteOnSessionClose = true)] public class BankService : IBankService { [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)] public void OutMoney(int fromAccountId, int money) { try { AccountRepository repository = new AccountRepository(); Account accOut = repository.GetAccountById(fromAccountId); accOut.Money -= money; repository.Save(accOut); } catch (Exception ex) { System.Transactions.Transaction.Current.Rollback(); throw new FaultException(new FaultReason(ex.Message)); } } [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)] public void InMoney(int toAccountId, int money) { try { AccountRepository repository = new AccountRepository(); Account accIn = repository.GetAccountById(toAccountId); accIn.Money += money; repository.Save(accIn); } catch (Exception ex) { System.Transactions.Transaction.Current.Rollback(); throw new FaultException(new FaultReason(ex.Message)); } } }
复制代码

  服务操做的执行是否须要自动登记到事务当中,以及服务操做什么时候提交,是服务端本身说了算的,因此要在具体的操做上设定操做行为。这里咱们用到两 个行为:TransactionScopeRequired和TransactionAutoComplete,它们都是布尔值,前者用于决定操做是否纳 入事务内,默认为false,这里须要设置为true,后者用于决定该操做执行完毕后是否提交事务,因而在第一个操做上设置为false,第二个操做设置 为true。

  在ServiceBehavior上能够设定事务的行为,

  最后须要在事务抛异常的状况下回滚。TransactionIsolationLevel用于指定事务隔离级别,默认是 Serializable,TransactionTimeout不解释,TransactionAutoCompleteOnSessionClose 表示在会话正常结束时是否自动提交事务,默认为false,另外还有一个 ReleaseServiceInstanceOnTransactionComplete,表示当事务完毕时是否须要释放服务实例,默认为false。

  写完了服务端代码就该改XML了,XML以下:

复制代码
<configuration> <system.serviceModel>  <bindings> <ws2007HttpBinding> <binding name="transactionalTcpBinding" transactionFlow="true" /> </ws2007HttpBinding> </bindings> <behaviors> <serviceBehaviors> <behavior name="metadataBehavior"> <serviceMetadata httpGetEnabled="true" httpGetUrl="http://127.0.0.1:9527/bankservice/metadata" /> </behavior> </serviceBehaviors> </behaviors> <services> <service name="Bank.Service.BankService" behaviorConfiguration="metadataBehavior" > <host> <baseAddresses> <add baseAddress="http://127.0.0.1:9527/"/> </baseAddresses> </host> <endpoint address="bankservice" binding="ws2007HttpBinding" bindingConfiguration="transactionalTcpBinding" contract="Bank.Interface.IBankService" /> </service> </services> </system.serviceModel> </configuration>
复制代码

  WCF是否有能力流转事务以及事务按照怎样的协议流转,是绑定控制的。在WCF当中,除了BasicHttpBinding、 NetMsmqBinding和MsmqIntegrationBinding外,都是支持事务传播的(下P145),即使支持事务,事务流转也是默认关 闭的,因此须要配置绑定的transactionFlow属性。这里使用了ws2007HttpBinding,并将其transactionFlow属 性设置为true。

  发布服务后,在客户端使用以下代码调用服务:

复制代码
static void Main(string[] args) { BankServiceClient proxy = new BankServiceClient(); using (TransactionScope transactionScope = new TransactionScope()) { try { proxy.OutMoney(1, 100); proxy.InMoney(2, 100); transactionScope.Complete(); } catch (Exception ex) { (proxy as ICommunicationObject).Abort(); } } }
复制代码

  这里定义了一个事务范围,而且在最后提交了事务,若是出现异常,则关闭服务代理。这样,在服务过程中若是出现了异常,事务就会回滚了。

 

  十9、可靠会话

  此次真的是名副其实地把书读薄了!下册书上全是原理,嗯,这里不抄原理,只用来拷代码,不过发现书上木有太多现成可用的代码,因而就稍微总结总 结好了。所谓可靠会话就是用于保证消息传递有效、有序、不重复的一套机制,WCF对这套机制的实现体如今一个叫 ReliableSessionBindingElement的绑定元素上,因此要实现可靠会话,能够修改绑定,或者手写绑定。

  典型的应用就是文件分段传输,若是不实现可靠会话,分段传输就可能发生丢包、接收发送顺序不一致和重复发送的问题。

  WCF已经为咱们提供了不少支持可靠会话的内置绑定,其中wsHttpBinding、wsFederationBinding、 netTcpBinding的可靠会话功能是默认关闭的,wsDualHttpBinding和netNamedPipesBinding是默认开启的。

  启动可靠会话很简单,只要在绑定里加上配置:

复制代码
<bindings> <netTcpBinding> <binding name="reliableNetTcpBinding"> <reliableSession enabled="true"/> </binding> </netTcpBinding> </bindings>
复制代码

而且在终结点的bindingConfiguration属性指定这个绑定配置就好了。须要注意客户端和服务端的绑定配置要一致。  

  这个绑定配置节有几个属性,参考http://msdn.microsoft.com/zh-cn/library/ms731302.aspx,能够用于作负载控制,不过不管如何,ordered要设置为true才能开启可靠会话(P255)。

  另外,若是咱们须要某个服务操做必需要保证有序性才能被执行,则须要在ServiceContract接口定义上多打上一个反射标签:

[DeliveryRequirements(RequireOrderedDelivery = true)]

这样一来,若是没有在服务行为上配置有序,则host时会报异常。

相关文章
相关标签/搜索