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時會報異常。

相關文章
相關標籤/搜索