WCF z

终结点与服务寄宿

由于最近可能要使用WCF做开发,开始重读蒋金楠的《WCF全面解析》,并整理个人学习WCF的笔记。

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

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

  不过不得不说的是,对我这样的新手,读完了书中对WCF类结构的分析,对自己的框架设计能力的确起到了一定促进作用。

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

 

一、WCF在企业架构中的位置

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

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

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

   

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

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

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

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

 

二、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上,网上有不少帖子,这里就不赘述了。 

 

    三、配置终结点地址

    上一步看到的服务端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)。

 

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

  将某个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)。

 

五、信道与绑定

  在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提供的一种扩展机制,这些东西一旦用上只能查书看帖子来配置,所以贴代码是没意义的。

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

 

六、服务契约与操作契约

  接口提取的是变动中的“不变”部分,同 样,契约则定义了服务对外的“承诺”,即消息交互的“结构”。作为服务的消费者,我们需要知晓服务的契约,按照契约规定的方式调用和传参,才能得到预期的 结果。调用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)。

 

    七、客户端代理类

  最后来回顾一下客户端,假定我们给服务起 的名字叫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类型就是信道的类型。

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

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

八、消息交换模式

  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>
复制代码

 

  九、实例与会话

  上面了例子里有一个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)一张图说明问题:

  

  

  十、并发

  服务行为的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)。

 

  十一、限流

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

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

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

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

十二、数据契约

  在实际应用当中数据不可能仅仅是以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]。

  

  十三、继承关系的序列化

  依旧是老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);
    }
}
复制代码

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

 

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

  不论服务端还是客户端,他们的之间发送的数据都是要序列化为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" />

 

  十五、消息契约

  其实利用数据契约已经能够很好地完成数据的传输了,而数据契约只能控制消息体,有时候我们想在数据传递过程 中添加一些额外信息,而不希望添加额外的契约字段,那么我们就得改消息报头,也就是说该使用消息契约了。读老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当中,是因为报头是与次序无关的。

  

  十六、消息编码

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

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

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

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

  如果我们需要自己改写编码的方式就需要改绑定的XML或者手写绑定了(P285)。

 

   十七、异常与错误契约

  接下来换另一个话题——异常处理。和普通服务器编程一样,在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的书,例子多,而且也讲了不少原理方面的内容,不过越读越觉得压力山大……这次来稍微整理整理事务和可靠会话的内容。

  

  十八、事务编程

  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();
        }
    }
}
复制代码

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

 

  十九、可靠会话

  这次真的是名副其实地把书读薄了!下册书上全是原理,嗯,这里不抄原理,只用来拷代码,不过发现书上木有太多现成可用的代码,于是就稍微总结总 结好了。所谓可靠会话就是用于保证消息传递有效、有序、不重复的一套机制,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时会报异常。

posted on 2015-10-06 14:39  武胜-阿伟  阅读(1732)  评论(2编辑  收藏  举报