zl程序教程

您现在的位置是:首页 >  后端

当前栏目

WCF技术剖析之十:调用WCF服务的客户端应该如何进行异常处理

2023-09-27 14:27:57 时间

在前面一片文章(服务代理不能得到及时关闭会有什么后果?)中,我们谈到及时关闭服务代理(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: }

 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服务在客户端的调用程序如下所示:


 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(), "http://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方法的时候则不会抛出任何异常(而实际上,服务代理的关闭与否对于数据报信道来讲,没有任何意义)。具体实现如下面的代码所示:


 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), "http://127.0.0.1:3721/calculatorservice"))
 11: { //......
 12: }
 13: }
 14: }
 15: }
 16:  

三、Close() V.S. Abort()

在这种情况下,一般会调用另一个方法: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方法:


 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: } 

 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(), "http://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
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。 原文链接