软件开发中最残酷的现实是纵然非常小心地完成的系统也有崩溃和不曾预料的场景发生。一个好的开发人员要保证在创建阻止问题发生的软件和处理由软件导致错误的能力的一个平衡。基于服务的分布式系统没有异常。事实上,基于服务的分布式系统通过引入诸如服务可用性,网络条件和服务版本兼容能力等加剧了这个问题。
异常是一个分布式系统的一个严重问题,它可能由很多情况导致。例如,一个调用方可能没有向一个服务方提供正确的或者完整的信息,一个服务方可能在尝试完成一个操作时遇到一个问题,或者一条消息被按照一个不支持的版本格式化过。
在本章,我们将讨论WCF中出现异常的影响以及WCF为通信和异常处理提供的特性。我们将描述异常和错误之间的区别,创建发送给一个调用方的错误的方式,以及在服务端和调用端处理异常的方式。最后,我们将描述在服务宿主端对异常进行集中处理的方式,在异常或者错误发生时捕捉不可期待的异常湖综合做一些额外处理,比如日志功能。
WCF异常处理的简要介绍
在我们探讨恰当地处理WCF服务和关联的客户端应用程序异常的细节之前,让我们看看在默认设置下抛出异常以后会发生什么。对所有WCF开发人员来说理解当你未对异常做任何处理时会发生什么事重要的。
一个WCF服务通常对底层商业库调用进行包装,而且在任何托管代码中都可以预料到的是,这些库可能对它们的调用者引发标准.NET 异常。异常会导致堆栈增加知道它们被一个层处理掉或者到达根应用程序的上下文,在这个点它们一般向调用程序,进程或者线程(取决于当前正在运行的应用的类型)返回致命错误提示。
尽管未处理异常对WCF本身来说不是致命错误,WCF认为它们提示服务端与客户端持续通信的能力中有一个严重问题。在那些情况下,WCF将会让服务信道异常,这意味着任何已有的会话(例如,对安全来说,可信赖消息或者共享状态)将会被打破。如果一个会话是服务调用的一部分,那么客户端信道将不再有用,需要为客户端重建客户端代理来继续调用服务。
通过SOAP进行WCF异常通信
服务实现逻辑或者服务宿主本身的内部架构的异常通常是基于CLR的异常类型。因为服务需要支持任意类型的客户端和服务端之间的通信而不在意它们使用什么技术,那些.NET 特有细节必须被转换成一个标准格式以便于彼此协作通信使用。
协作能力通过将那些平台相关的异常细节序列化成由简单对象访问协议(Simple Object Access Protocol, SOAP)标准描述的通用数据元素来保证。SOAP标准提供了一个可能出现在一条SOAP消息体中的错误元素。
在本章,我们会描述将异常作为错误从服务端到客户端通信的几种方式。SOAP错误元素的细节知识在这里是不必要的因为WCF架构抽象了那些细节,提供给我们很多方式来提供与对应的SOAP错误元素和属性关联的额外信息。
最低程度地,一条SOAP错误必须提供两个值。reason 是错误条件的描述。另外一个需要的值是一个错误代码,它可以是一个自定义的指示器或者在SOAP标准中的预定义代码集合。我们将在稍后我们讨论FaultException类型时返回到这些概念。
SOAP标准的更多关于错误管理功能可以在W3C站点www.w3.org/TR/2007/REC-soap12-part0-20070427/#L11549 中找到。
未处理异常的例子
为了查看当未处理异常返回到服务宿主时WCF的行为,创建一个基本的WCF服务和最小化的Windows 客户端。为了描述服务端信道异常的影响,确保你的服务中有会话功能,例如通过选择wsHttpBinding, 它创建一个安全会话。
在服务实现部分,创建一个类似于列表10.1显示的操作。
列表10.1 契约示例和实现
namespace ServiceLibrary { [ServiceContract] public interface IService { [OperationContract] double Divide(double numerator, double denominator); } public class Service1 : IService { public double Divide(double numerator, double denominator) { if (denominator == 0) { throw new ArgumentOutOfRangeException("denominator", "Must be a numeric value less than or greator than zero"); } return numerator / denominator; } } }
从Windows客户端应用程序,使用添加服务引用选项来为服务端创建一个代理。创建一个有两个文本框和两个按钮的简单窗体程序。将第一个按钮关联到调用Divide网络服务上,通过文本框传输参数值。将第二个按钮关联到刷新服务代理本地实例上。你的客户端代码看起来可能与列表10.2中的类似。
列表10.2 客户端Windows应用程序代码
namespace WindowsClient { public partial class Form1 : Form { Service1.ServiceClient _serviceProxy = new WindowsClient.Service1.ServiceClient(); public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { try { MessageBox.Show(_serviceProxy.Divide(double.Parse(txtInputA.Text), double.Parse(txtInputB.Text)).ToString()); } catch (FaultException exp) { MessageBox.Show(exp.Code.Name + ": " + exp.Message.ToString(), exp.GetType().ToString()); } } private void cmdNewProxy__Click(object sender, EventArgs e) { _serviceProxy = new WindowsClient.Service1.ServiceClient(); } } }
运行这个应用程序并传输程序参数。假设你已经正确地完成了示例程序,那么除法结果会被显示出来。现在将被除数的值改为0然后重试一次调用过程。你应该看到一个类似于图片10.1的结果。
图片10.1 通过调用一个被除数是0的服务返回FaultException
最后,将被除数设置为一个非零值并再次调用服务。尽管调用成功完成了,但是调用却失败了,接收到一条类似于图片10.2 的CommunicationObjectFaultedException的错误消息。
图片10.2 错误信道的CommunicationObjectFaultedException
因为WCF在服务宿主接收到一个未处理异常,它假设这个异常暗示一个致命错误并因此让服务信道失败。在我们的例子中使用wsHttpBinding,创建的安全会话不再合法,所以通信必须通过重建客户端代理来重建。
注意 单向操作和错误 设计为单向目的地的操作不从调用的服务接收一条消息,不考虑这次调用是否成功。因为没有消息返回,客户端就收不到任何错误已经发生的提示。 额外的,如果错误由未一个未处理异常引发,服务信道将持续错误,但是客户端将不会意识到这个事实。在一个会话独立的交互中,持续调用将会失败(CommunicationObjectFaultException)直到代理被重新创建了。 |
检查并发现一个错误信道
错误信道可以也应该被客户端检查。客户端代码应该在每次错误发生之后检查信道来确定是否那个错误导致信道本身的错误。这可以通过让客户端代码在如列表10.3中所示的异常处理代码中检查信道状态属性来完成。
列表10.3 确定一个信道没有失败
namespace WindowsClient { public partial class Form1 : Form { Service1.ServiceClient _serviceProxy = new WindowsClient.Service1.ServiceClient(); public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { try { MessageBox.Show(_serviceProxy.Divide(double.Parse(txtInputA.Text), double.Parse(txtInputB.Text)).ToString()); } catch (FaultException exp) { MessageBox.Show(exp.Code.Name + ": " + exp.Message.ToString(), exp.GetType().ToString()); if (_serviceProxy.State == CommunicationState.Faulted) { MessageBox.Show("Communication channel has been faulted. Attempting to recover."); cmdNewProxy__Click(null, null); } } catch (CommunicationException exp) { MessageBox.Show("Communication error: " + exp.Message.ToString(), exp.GetType().ToString()); } catch (Exception exp) { MessageBox.Show("General error: " + exp.Message.ToString(), exp.GetType().ToString()); } } private void cmdNewProxy__Click(object sender, EventArgs e) { _serviceProxy = new WindowsClient.Service1.ServiceClient(); } } }
如果检测出一个错误状态,你应该记录这个问题发生的条件和原因,并尝试重建客户端代理,然后继续。当那样做不可行时,比如当一个会话正在进行中那么你就不可以手动重建客户端,用户应该被通知且使用那个代理进行进一步调用会被阻止。