閱讀787 返回首頁    go 阿裏雲 go 技術社區[雲棲]


WCF技術剖析之十:調用WCF服務的客戶端應該如何進行異常處理

在前麵一片文章(服務代理不能得到及時關閉會有什麼後果?)中,我們談到及時關閉服務代理(Service Proxy)在一個高並發環境下的重要意義,並闡明了其根本原因。但是,是否直接調用ICommunicationObject的Close方法將服務代理關閉就萬事大吉了呢?事情遠不會這麼簡單,這其中還會涉及關於異常處理的一些操作,這就是本篇文章需要討論的話題。

一、異常的拋出與Close的失敗

一般情況下,當服務端拋出異常,客戶客戶端的服務代理不能直接關閉,WCF在執行Close方法的過程中會拋出異常。我們可以通過下麵的例子來證實這一點。在這個例子中,我們依然沿用計算服務的例子,下麵是服務契約和服務實現的定義:

   1: using System.ServiceModel;
   2: namespace Artech.ExceptionHandlingDemo.Contracts
   3: {
   4:     [ServiceContract(Namespace = "urn:artech.com")]
   5:     public interface ICalculator
   6:     {
   7:         [OperationContract]
   8:         int Divide(int x, int y);
   9:     }
  10: }

 

   1: using System;
   2: using System.ServiceModel;
   3: using Artech.ExceptionHandlingDemo.Contracts;
   4: namespace Artech.ExceptionHandlingDemo.Services
   5: {
   6:     class CalcualtorService : ICalculator { public int Divide(int x, int y) { return x / y; } }
   7: }

為了確保服務代理的及時關閉,按照典型的編程方式,我們需要采用try/catch/finally的方式才操作服務代理對象,並把服務代理的關閉放在finally塊中。WCF服務在客戶端的調用程序如下所示:

   1: using System;
   2: using System.ServiceModel;
   3: using Artech.ExceptionHandlingDemo.Contracts;
   4: namespace Client
   5: {
   6:     class Program
   7:     {
   8:         static void Main(string[] args)
   9:         {
  10:             using (ChannelFactory<ICalculator> channelFatory = new ChannelFactory<ICalculator>(new WSHttpBinding(), "https://127.0.0.1:3721/calculatorservice"))
  11:             {
  12:                 ICalculator calcultor = channelFatory.CreateChannel(); try
  13:                 {
  14:                     calcultor.Divide(1, 0);
  15:                 }
  16:                 catch (Exception ex) { Console.WriteLine(ex.Message); }
  17:                 finally
  18:                 {
  19:                     (calcultor as ICommunicationObject).Close();
  20:                 }
  21:             }
  22:         }
  23:     }
  24: }

由於傳入的參數為1和0,在服務執行除法運算的時候,會拋出DividedByZero的異常。當服務端程序執行到finally塊中對服務代理進行關閉的時候,會拋出如下一個CommunicationObjectFaultedException異常,提示SerivceChannel的狀態為Faulted,不能用於後續Communication。

image

要解釋具體的原因,還得從信道(Channel)的兩種分類形式說起。在上麵一篇文章中,我們就談到過:WCF通過信道棧實現了消息的編碼、傳輸及基於某些特殊功能對消息的特殊處理,而綁定對象是信道棧的締造者,不同的綁定類型創建出來的信道棧具有不同的特性。就對會話的支持來講,我們可以將信道分為以下兩種:

  • 會話信道(Sessionful Channel):會話信道確保客戶端和服務端之間傳輸的消息能夠相互關聯,但是信道的錯誤(Fault)會影響後續的消息交換;
  • 數據報信道(Datagram Channel):即使在同一個數據報信道中,每次消息的交換都是相互獨立,信道的錯誤也不會影響後續的消息交換。

由於上麵的例子中,我們采用了WsHttpBinding,所以在默認條件下創建的信道(Channel)是會話信道(Sessionful Channel)。異常拋出後,當前信道的狀態將變成Faulted,表示信道出現錯誤。錯誤的信道將不能繼續用於後續的通信,即使是調用Close方法試圖將其關閉也不行。

也就是說異常導致信道錯誤(Faulted)的特性僅僅對於會話信道而言,而對於數據報信道,則沒有這樣的問題。對於WsHttpBinding在如下兩種情況下下具有創建會話信道的能力:

  • 采用任何一種非None的SecurityMode
  • 采用ReliableSession

再默認的情況下,WsHttpBinding采用的SecurityMode為Message,所以其創建的信道是會話信道。如果我們將其SecurityMode設為None,則在執行Close方法的時候則不會拋出任何異常(而實際上,服務代理的關閉與否對於數據報信道來講,沒有任何意義)。具體實現如下麵的代碼所示:

   1: using System;
   2: using System.ServiceModel;
   3: using Artech.ExceptionHandlingDemo.Contracts;
   4: namespace Client
   5: {
   6:     class Program
   7:     {
   8:         static void Main(string[] args)
   9:         {
  10:             using (ChannelFactory<ICalculator> channelFatory = new ChannelFactory<ICalculator>(new WSHttpBinding(SecurityMode.None), "https://127.0.0.1:3721/calculatorservice"))
  11:             {               //......
  12:             }
  13:         }
  14:     }
  15: }
  16:  

在這種情況下,一般會調用另一個方法:Abort,強行中斷當前信道。一般情況下,對於客戶端來說,信道在下麵兩種情況下狀態會變成Faulted:

  • 調用超時,拋出TimeoutException
  • 調用失敗,拋出CommunicationException

所以正確的客戶端進行服務調用的代碼應該如下麵的代碼所示:通過try/catch控製服務調用,在try控製塊中進行正常服務調用並正常關閉服務代理進程(調用Close方法);在catch控製塊中,捕獲CommunicationException和TimeoutException這兩個異常,並將服務代理對象強行關閉(調用Abort方法)。下麵的代碼演示了基於ChannelFactory<T>創建服務代理的WCF客戶端編程方式,對於直接通過強類型服務代理(繼承ClientBase<T>的服務代理類型)進行服務調用具有相同的結構。

   1: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("calculateservice"))
   2: {
   3:     ICalculator calculator = channelFactory.CreateChannel();
   4:     try
   5:     {
   6:         int result = calculator.Divide(1, 0);
   7:         (calculator as ICommunicationObject).Close();
   8:     }
   9:     catch (CommunicationException ex)
  10:     {
  11:         //Exception Handling
  12:         (calculator as ICommunicationObject).Abort();
  13:     }
  14:     catch (TimeoutException ex)
  15:     {
  16:         //Exception Handling
  17:         (calculator as ICommunicationObject).Abort();
  18:     }
  19:     catch (Exception ex)
  20:     {
  21:         //Exception Handling
  22:     }
  23: }  

四、通過一些編程技巧避免重複代碼

如果嚴格按中上麵的編程方式對CommunicationException和TimeoutException進出捕獲和處理,那麼你的客戶端代碼就會到處充斥中相同的代碼片斷。我不知一次說過,如果你的代碼中重複頻率過高,或者編程人員廣泛地采用Ctrl+C|Ctrl+V這樣的編程方式,那麼這就是你進行代碼重構的信號。

為此,我們可以通過對Delegate的利用來進行代碼的分離(服務調用代碼和異常處理代碼)。比如,我寫了下麵兩個Invoke方法:

   1: using System;
   2: using System.ServiceModel;
   3: using Artech.ExceptionHandlingDemo.Contracts;
   4: namespace Client
   5: {
   6:     class Program
   7:     {
   8:         static void Invoke<TContract>(TContract proxy, Action<TContract> action)
   9:         {
  10:             try
  11:             {
  12:                 action(proxy);
  13:                 (proxy as ICommunicationObject).Close();
  14:             }
  15:             catch (CommunicationException)
  16:             {
  17:                 (proxy as ICommunicationObject).Abort();
  18:                 //Handle Exception
  19:                 throw;
  20:             }
  21:             catch (TimeoutException )
  22:             {
  23:                 (proxy as ICommunicationObject).Abort();
  24:                 //Handle Exception
  25:                 throw;
  26:             }
  27:             catch (Exception)
  28:             {
  29:                 //Handle Exception
  30:                 (proxy as ICommunicationObject).Close();
  31:             }
  32:         }
  33:  
  34:         static TReturn Invoke<TContract, TReturn>(TContract proxy, Func<TContract, TReturn> func)
  35:         {
  36:             TReturn returnValue = default(TReturn);
  37:             try
  38:             {
  39:                returnValue =  func(proxy);
  40:             }
  41:             catch (CommunicationException)
  42:             {
  43:                 (proxy as ICommunicationObject).Abort();
  44:                 //Handle Exception
  45:                 throw;
  46:             }
  47:             catch (TimeoutException)
  48:             {
  49:                 (proxy as ICommunicationObject).Abort();
  50:                 //Handle Exception
  51:                 throw;
  52:             }
  53:             catch (Exception)
  54:             {
  55:                 //Handle Exception
  56:             }
  57:  
  58:             return returnValue;
  59:         }
  60:     }
  61: } 

那麼,對CalculatorService就可以簡化成:

   1: using System;
   2: using System.ServiceModel;
   3: using Artech.ExceptionHandlingDemo.Contracts;
   4: namespace Client
   5: {
   6:     class Program
   7:     {
   8:         static void Main(string[] args)
   9:         {
  10:             using (ChannelFactory<ICalculator> channelFatory = new ChannelFactory<ICalculator>(new WSHttpBinding(), "https://127.0.0.1:3721/calculatorservice"))
  11:             {
  12:                 ICalculator calcultor = channelFatory.CreateChannel(); int result = Invoke<ICalculator, int>(calcultor, proxy => proxy.Divide(2, 1));                   //......               
  13:             }
  14:         }
  15:     }
  16: }

 



作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-30 14:04:11

  上一篇:go  未來互聯網醫療模式探討
  下一篇:go  手把手:教你用Scrapy建立你自己的數據集