WCF4.0进阶系列--第三章 构建健壮的程序和服务
【前言】
捕获和处理异常是任何专业的应用程序的一个重要组成部分。在一个复杂的桌面程序上,许多场景都能产生一个异常,从程序错误、意外的事件、不完整的用户输入,到运行该程序一个或多个的硬件故障。在分布式环境中,异常的发生场景就更多了。因为网络的状态,在某些情况下,无论应用程序开发人员、还是网络管理员都无法控制网络的发挥功效或者维持其处于正常的工作状态。如果把应用程序访问第三方组件考虑在内,那么还应考虑你的分布式程序能否可靠地工作.
本章将介绍如何处理客户端和服务端的异常。你将学习到如何在服务端指定异常,以使服务能引发该异常,并将该异常的信息推送至客户端。你还将了解到服务可能所处的状态,以及如何确定宿主程序是否从一个状态切换到另一个状态,以及如何使服务从一个失败的状态中恢复。最后,你还将看到如何识别客户端发送给服务端的预料之外的消息。
【正文】
CLR异常与SOAP Faults
WCF服务是运行在.NET Framework CLR上的托管程序。CLR的重要特性是当托管程序发生错误时为其提供保护;CLR能识别许多系统级的错误,并在必要时引发异常。托管程序试图去捕获这些异常并试图采取一些补救措施或者平稳地失败(报告异常的原因,为开发人员提供信息以帮助理解异常的根本原因,并采取行动纠正错误)。
SOAP规范有一部分内容专门讲述了在SOAP消息中如何使用SOAP Faults来格式化并发送错误。SOAP规范还包含了一个Schema将SOAP faults转换为XML文本并将其封装成一个SOAP消息。SOAP fault必须指定一个错误代码,fault的文字描述,及其他可选信息。使用WCF构建的支持互操作的服务将.NET Framework异常转换为SOAP faults,然后遵循SOAP规范向客户端报告并发送这些faults。CLR异常是.NET Framework独有的,而WCF目的在于构建能客户端和服务端与其他环境互操作。很显然,Java客户端不能直接识别或处理WCF服务触发的CLR异常,那么该怎么办呢?在WCF服务端,通过将CLR异常转化成SOAP faults消息回传给客户端,这样Java客户端便能识别这些faults消息的内容,以捕获WCF服务的异常。更多关于SOAP fault的内容,请参考 http://www.w3.org/TR/soap12-part1/#soapfault
(1)(服务端)抛出和(客户端)捕获SOAP Fault
WCF类库在System.ServiceModel命名空间下提供了FaultException类。如果WCF服务抛出FaultException对象,WCF运行时将生成SOAP fault消息并回传给客户端程序。下图例举了与FaultException类有关联的类。
在本章的第一个练习中,你将添加代码至ProductService服务,使其在访问AdventureWorks数据库是能识别错误,并使用FaultException类报告这些问题给客户端。
修改WCF服务使其抛出SOAP Faults
1创建一个ProductsServiceFault的空白解决方案,并复制第二章ProductsServiceLibrary下的所有项目到此解决方案中,并将它们添加到ProductsServcieFault方案中,结果将如下图所示:
2打开ProductsServiceLibrary项目下的ProductsService.cs文件,修改ListProducts方法:
catch (Exception ex)
{
if (ex.InnerException is System.Data.SqlClient.SqlException)
throw new FaultException(string.Format("Exception accessing database:{0}",
ex.InnerException.Message), new FaultCode("Connect to database"));
else
throw new FaultException(string.Format("Exception reading from numbers: {0}",
ex.Message), new FaultCode("Iterate through products"));
}
如果发生异常,上面的代码将检查异常的原因。如果Exception对象的InnerException属性为SqlException,那么异常是由于Entity Framework访问数据库的代码引起。如果为其他原因,那么异常必定存在于迭代从数据库读取到产品列表的代码片段中。在这两种情况下,上述代码都会产出一个新的FaultException对象,并指定该FualtException对象的详细信息,然后再抛出FaultExceptio对象。抛出异常后,ListProducts操作停止运行,WCF运行时将生成一个SOAP fault,该fault包含了该异常的原因,编码(在本例中,我们简单地制定了一个名字用于区分不同的编码)的详细信息。最后,WCF运行时把SOAP fault回传至客户端。
3生成项目,确保没有错误和异常。
修改客户端使其能捕获SOAP Faults
1打开ProductsClient的Programm.cs文件,修改代码
try
{...
}
catch (FaultException ex)
{
Console.WriteLine("{0}: {1}", ex.Code.Name, ex.Reason)
}
如果你调用的任意一个操作生成了SOAP Fault,在客户端的WCF运行时将创建一个FaultException对象。客户端异常捕获处理器捕获该FaultException对象,并显示他的Code.Name和Reason属性的值。Code.Name是通过服务端的代码中指定的,Reason的值则来自服务端提供的关于异常原因的文字描述。
测试捕获FaultException异常
1打开ProductsServiceHost的app.config文件,修改数据库的链接字符串所连接的数据库名字为Junk。
2生成并运行该项目(非调试模式下运行:转到bin\debug文件夹,双击ProductsServiceHost.exe)。
3点击"start"按钮启动服务ProductsService。
4启动ProductsClient(转到bin\debug目录,双击ProductsClient.exe),你将会得到如下结果:
5退出ProductsClient, 并修改ProductsServcieHost项目下的app.config中的连接字符串到AdventureWorks;
6修改ProducsServiceLibrary项目下的ProductsService中ListProducts方法:
List<string> productsList = null;
try
{
using (AdventureWorksEntities database = new AdventureWorksEntities())
{
var products = from product in database.Products
select product.ProductNumber;
productsList.Clear();
productsList = products.ToList();
}
}
7生成解决方案,并确保没有错误和异常。
8再次先执行ProductsServiceHost,点击"Start"按钮启动服务ProductsService;
9再次运行ProductsClient,你将会得到如下结果:
(2)使用强类型Faults
抛出FaultException非常简单,但实际上它并不是像它第一次出现那么有用。客户端程序必须检查它捕获到的FaultException对象以确定异常的错误的原因;因此,调用WCF服务时预料可能发生的异常是不容易地。在这种情况下,所有开发人员能做的就是编写一段通用的异常处理器在很小范围内让程序实现从特定的异常中恢复过来。你可以认为这和普通.NET Framework应用程序使用System.Exception类型去抛出和捕获异常是相似的。一个更好的解决方案是使用强类型的SOAP faults
在第一章中,你可以看到WCF服务的服务契约包含一系列定义在服务实现类方法上的操作契约。服务契约可以包含执行操作时可能发生的faults信息。如果WCF服务的一个操作侦查到一个异常,它便生成一个特定的可回传至客户端程序的SOAP fault消息。该SOAP fault消息应包含足够详细的信息以供用户或管理员能理解异常发生的原因,而且,如果可能,采取必要的纠正行为。客户端程序能使用服务契约中预计的faults信息,并提供特定的处理器去捕获和处理每个不同的faults。这些不同的faults就是强类型(非托管对象)faults。
你在服务契约上,通过FaultContract特性来指定可能发生的faults。下面这个练习,我们将做这件事情。
使用FaultContract特性为操作指定指定SOAP Faults
1打开ProductsServiceFault解决方案中ProductsServieLibrary项目的IProductsService.cs文件
2在IProductsService.cs中添加两个类SystemFault和DatabaseFault
[DataContract]
public class SystemFault
{
[DataMember]
public string SystemOperation { get; set; }
[DataMember]
public string SystemReason { get; set; }
[DataMember]
public string SystemMessage { get; set; }
}
[DataContract]
public class DatabaseFault
{
[DataMember]
public string DbOperation { get; set; }
[DataMember]
public string DbReason { get; set; }
[DataMember]
public string DbMessage { get; set; }
}
你将使用上述两个类型来传递SOAP faults的详细内容作为异常从服务端回传给客户端。请注意,尽量这两个类"外形"十分相似,但你却可以在SOAP faults内传递这两者中任何一个类型的信息。只要该类型和它的成员能被序列化。从上面代码,我们可以看到,这两个类都指定了DataContract和DataMember特性指定它们能被序列化。
3修改IProductsService接口,为ListProducts添加FaultContract特性
[FaultContract(typeof(SystemFault))]
[FaultContract(typeof(DatabaseFault))]
[OperationContract]
List<string> ListProducts();
FaultContract特性表明ListProducts方法能生产SOAP faults, 这样客户端便可以做好相应的准备工作来处理这些faults。FaultContract的参数指明SOAP fault将包含的信息。在本例中,一个是SystemFault类型,另一个是DatabaseFault类型。
修改WCF服务,使其抛出强类型Faults
1打开ProductsService.cs文件,并找到方法ListProducts。
2 使用下列代码代替之前catch代码段if的代码。
if (ex.InnerException is System.Data.SqlClient.SqlException)
{
DatabaseFault dbf = new DatabaseFault()
{
DbOperation = "Connect to database",
DbReason = "Exception accessing database",
DbMessage = ex.InnerException.Message
};
throw new FaultException<DatabaseFault>(dbf);
}
上述代码段创建和推送异常,该异常的详细信息是一个DatabaseFault对象。Throw语句基于DatabaseFault创建一个新的FaultException对象。 请注意在本例中,代码使用了generic FaultException类; 类型参数指明了一个可序列化的类型,其包含了特定类型异常(DatabaseFault)的详细信息。WCF运行时,将使用该对象内部的信息创建SOAP fault消息。 FaultException重载了构造器,与DatabaseFault对象一样,你可选择性指定一个原因消息和fault编码。
3使用下列代码替换catch代码段else的代码。
else
{
SystemFault sf = new SystemFault()
{
SystemOperation = "Iterate through products",
SystemReason = "Exception reading product numbers",
SystemMessage = ex.Message
};
throw new FaultException<SystemFault>(sf);
}
上述代码与第二步的代码相似,差别就是上述代码创建了SystemFault对象,并基于SystemFault对象抛出一个generic的FaultException,FaultException的类型参数是SystemFault。
4生成项目并确保没有错误和异常。
现在你可以修改客户端程序来处理服务端抛出的异常。但是,首先你必须重新生成代理类,该代理类用来与服务端进行通信。ProductsService当前并没有运行。因此你不能使用Visual Studio的"更新服务引用"特性。你将使用svcutil工具来从包含ProductsServcie服务的组件生成其代理类。
为WCF客户端程序ProductsClient重新生成代理类
1用管理员身份打开Visual Studio Command Prompt,并转到ProductServiceLibrary的bin\debug (D:\Works\Solutions\WCF\Step.by.Step\Chapter3\ProductsServiceFault\ProductsServiceLibrary\bin\Debug),请根据实际地址调正位置。然后执行下面的命令:svcutil ProductsServiceLibrary.dll
2命令执行的结果将产生下表所列的文件。
文件 |
描述 |
Products.xsd |
这是一个XML schema文件,其描述DatabaseFault,SystemFault和ProductData类型。 Svcutil工具根据数据契约来生成这三个文件。 |
Tempuri.org.xsd |
另外一个xml schema文件,其描述客户端与ProductsService服务端接收和发送的消息。在后面你将看到服务的每个操作都定义了一对消息: 一个消息用于指定客户端调用该操作而必须想服务端发送的消息;另一个指定服务端回传至客户端的响应消息。该文件引用Products.xsd文件中的数据契约以获取调用GetProduct操作而从服务端返回的响应中的ProductData类型的描述。 |
Schema.microsoft.com.-2003.10.Serialization.Arrays.xsd |
一个xml schema文件,用以描述如何在SOAP消息中使用字符数组。ListProducts操作将引用该操作对应的响应消息中的信息。ListProducts操作的结果是包含Product numbers的字符串列表。在第一章我们已经叙述了,.NET Framework的generic List<>类型在SOAP消息传输的过程中将被序列化为一个数组 |
Schema.microsoft.com.-2003.10.Serialization.xsd |
该xml schema文件描述基本类型(比如float,int,deciaml和string)在SOAP消息中如何呈现。当然还包括.NET Framework经常使用的内置的类型是如何在SOAP消息中呈现的。 |
Tempuri.org.wsdl |
该文件包含了服务的WSDL定义,其描述了消息和数据契约是如何实现服务端的操作,并供客户端调用的这些操作。该文件引用Products.xsd和Tempuri.org.xsd文件。请注意,ListProducts操作的定义包括之前已经定义的两个Faults消息。你可以使用WSDL文件和XML schema文件生成代理类。 |
3使用svcutil执行下列命令,以生成代理类。
上述命令使用WSDL文件和所有的schema文件生成一个C#源文件,该源文件包含一个类,它就是服务的代理对象;命名空间参数用以指定该代理对象类的命名空间。命令的执行结果将产生两个文件
文件 |
描述 |
Products.cs |
代理类的源文件 |
Output.config |
应用程序配置文件,客户端应用程序能使用它来配置代理对象,使其能与服务通信。默认情况下,该配置文件将包含一个采用basicHttpBinding绑定的端点。 |
有关svcutil.exe详细信息,请参考MSDN:http://msdn.microsoft.com/en-us/library/aa347733.aspx
4在ProductsClient项目中,备份app.config。
5在ProductsClient项目中,删除服务引用。该操作的结果将删除app.config中的相应信息,这也是执行备份app.config的原因。
6添加之前使用svcutil工具生成的products.cs文件到ProductsClient项目。
7删除app.config,并还原之前备份的app.config.
8打开还原后的app.config,然后修改端点契约的值为"ProductsClient.ProductsService.IProductsService"。
修改WCF客户端以捕获强类型Faults
1打开ProductClient项目的program.cs文件
2添加下面两个catch代码段
try
{
...
}
catch (FaultException<DatabaseFault> dbf)
{
Console.WriteLine("DatabaseFault {0}: {1}\n{2}",
dbf.Detail.DbOperation,
dbf.Detail.DbMessage,
dbf.Detail.DbReason);
}
catch (FaultException<SystemFault> sf)
{
Console.WriteLine("SystemFault {0}: {1}\n{2}",
sf.Detail.SystemOperation,
sf.Detail.SystemMessage,
sf.Detail.SystemReason);
}
catch (FaultException ex)
{
Console.WriteLine("{0}: {1}", ex.Code.Name, ex.Reason);
}
3生成项目,确保没有异常和错误
4先执行ProductsServiceHost,点击"Start"按钮启动服务ProductsService;然后启动ProductsClient;你将会得到如下结果:
5修改ProductsServiceLibrary项目下的ProductsService文件中的ListProducts方法
public List<string> ListProducts()
{
List<string> productsList = new List<string>();
try
{
using (AdventureWorksEntities database = new AdventureWorksEntities())
{
var products = from product in database.Products
select product.ProductNumber;
productsList = products.ToList();
}
}
catch (Exception ex)
{
... }
return productsList;
}
6修改ProductsServiceHost项目下的app.config,修改数据的名字为Junk(这个数据库在服务器上不存在)
7生成项目,确保没有错误或异常
8再次执行ProductsServiceHost,点击"Start"按钮启动服务ProductsService;然后启动ProductsClient;你将会得到如下结果:
(3)报告意外预料之外的异常
当执行服务端的操作时指定可能抛出的异常,是服务契约的重要组成部分。如果你使用强类型的异常,你必须在该服务操作的服务契约中指定所能抛出的每一种异常。如果服务抛出了一个未在服务契约中指定的强类型异常,该异常的详细信息将不会被推送至客户端,因为该异常未包含在用于生成代理类的WSDL描述文件中。预料一个操作可能出现的所有异常无疑是一件非常困难的事情。在这种情况下,你需要捕获服务抛出的预料之外的异常,并传送至客户端,然后触发一个普通的(非generic)FaultException。
在你开发WCF服务时,为了在客户端程序调试,将会把服务端发生的所有异常(包括预料之内的和预料之外的)转换成SOAP faults消息传送至客户端是非常有用的。在下面的练习中,你将完成上述目标。
修改WCF服务抛出预料之外的异常
1打开ProductsServiceFault解决方案,并打开ProductsServiceLibrary下的ProductsService.cs文件,修改ListProducts方法:
public List<string> ListProducts()
{
int i = 0, j = 0, k = i / j;
...
}
2生成项目,启动ProductsServiceHost,然后启动服务,最后运行测试项目ProductCluient,你将会得到如下结果:
上诉实验的结果,服务端把异常的完整信息推送至客户端,这在安全方面是有隐患的。如果服务端为客户端提供了完整的异常信息,根据这些信息,别有用心的用户将收集该服务的架构及其与内部工作原理相关的有用信息,然后对服务实施攻击。
配置WCF服务使其发送详细的异常 (这个标题其实有歧义,作者的本意应该是为异常提供一个安全的异常信息,而不是该异常的技术方面的信息,这个有点不好解释,你看完下面这个练习,你应该就明白了。)
1 打开并修改ProductsServcieHost的app.config文件
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior>
<serviceDebug includeExceptionDetailInFaults="true" />
</behavior>
</serviceBehaviors>
</behaviors>
...
<system.serviceModel>
2 生成项目,确保没有错误和异常
3运行ProductsServiceHost,并点击"start"按钮启动服务
4运行ProductsClient,你将将会得到如下结果
上述的目标还可以通过指定服务实现类的ServiceBahavior特性来实现
[ServiceBehavior(IncludeExceptionDetailInFaults = true)]
public class ProductsService : IProductsServcie
{
...
}
但是通过配置文件的方式是推荐的使用的方式,原因有两个:第一,你可以直接在配置文件中打开或关闭这个特性而不需要重新生成你的程序。但当你将程序部署到产品环境中时,请关闭该特性。如果你通过代码方式来实现该特性,那么此时,你很容易忘记关闭该特性;第二,如果你通过代码方式来实现该特性,你不能通过配置文件来关闭该特性。更让你更迷惑的是,如果你用代码关闭了该服务,但是你可以通过配置文件来打开该特性。通用的规则是:如果代码或配置文件启用了该特性,那么该特性就被启动了;如果代码和配置文件都关闭了该特性,那么该特性才被关闭。所以,为了简单,使用配置文件来打开或关闭该特性。
管理WCF服务宿主程序的异常
在第二章中,你了解到如何为WCF服务创建一个宿主程序,使用该宿主程序控制该服务的生命周期。宿主程序使用ServiceHost对象去实例化和管理WCF服务。ServcieHost类实现了"有限状态"机制。ServieHost对象能处于有限的几个状态之中。这些状态预先被定义,并被用来供WCF运行时将ServiceHost对象从一个状态转到到另一种状态。其中一些状态的转换是由于特定的方法被调用,而其他的则是由于服务的异常、通信架构的异常、堆栈中对象异常引起转换。宿主程序应该能处理这些转换并尽最大可能保证服务的可用性。
(1)ServiceHost状态和事务
当你实例化ServiceHost对象后,其状态转换成"Created"。在此状态下,你可以配置该对象,比如,你可以使用AddServiceEndpoint方法使ServiceHost对象开始侦听来自某个特定端点的请求,但ServiceHost对象此时还不能接收来自客户端的请求。
当你调用Open方法后,ServiceHost对象开始侦听请求(或者你可以使用异步编程模型下的BenginOpen方法)。当使用每个端点的绑定信息成功创建通道堆栈和启动服务后,ServiceHost的状态转换成Opening。如果此时发生异常,Servicehost的状态将转换成Faulted。如果ServiceHost成功的打开服务的通信通道,其状态转换为Opened状态。只有达到这个状态后,ServiceHost对象才能接收来自客户端的请求,并将这些请求引导至其寄宿的服务上。
当你调用Close方法之后,ServeiHost对象停止侦听客户端的请求。ServcieHost对象随之进入Closing状态。目前正在被处理的请求,将被允许执行完成,但是客户端不能再向服务发送新的请求。当所有等待处理的请求都执行完成后,ServiceHost对象转换至Closed。你还可以是用Abort方法停止服务,该方法立即停止服务,且不需等待服务完成正在被处理的请求。当ServiceHost处于Stopping或者Aborting状态时,ServiceHost对象所实例化的服务对象将销毁,其使用的资源也将被回收。为了再一次启动服务,你必须重新创建ServiceHost对象,该ServiceHost实例化一个新的Servcie对象,然后执行ServiceHost的Open方法重新创建通道堆栈,重新开始侦听请求。
如果调用ServiceHost的Open方法失败,或者服务与客户端通信的通道了遇到了无法恢复的错误,ServiceHost对象将转换至Faulted状态。在此状态下,你可以检查ServiceHost对象的属性来尝试获取失败的原因,但是不可以将客户端请求引导至服务。为了恢复服务,你应该使用Abort方法关闭服务,重新创建ServiceHost对象,然后在使用Open方法。下图总结了ServiceHost的状态转换。
(2)在宿主程序中处理Faults
当ServiceHost对象从一个状态切换到另一个状态时,它会触发一个相应的事件。
从上面代码我们可以看出,针对错误处理,ServiceHost则提供了Faulted事件,调用该事件后,ServiceHost对象进入Faulted状态。当发生错误的时候,你通过订阅该事件,然后使用其提供的方法确定发生错误的原因,最后抛弃现有的服务并重启一个新的服务。示例代码:
private void btnStart_Click(object sender, RoutedEventArgs e)
{
try
{
productsServiceHost = new ServiceHost(typeof(Products.ProductsService));
productsServiceHost.Open();
// subscribe the faulted event
productsServiceHost.Faulted += (eventSender, eventArgs) =>
{
productsServiceHost.Abort();
productsServiceHost = new ServiceHost(typeof(Products.ProductsService));
productsServiceHost.Open();
};
...
}
catch (Exception ex)
{
handleException(ex);
}
}
请注意,上面例子中使用了abort方法而不是close方法,原因在于当ServiceHost处于Faulted状态时,使用Abort方法能减少重启(其实是销毁当前ServiceHost,然后创建一个新的ServiceHost对象并打开该对象)ServiceHost所需的时间。
(3)在宿主程序中处理来自客户端的预料之外的消息
另一种异常发生的环境是宿主程序收到了来自客户端的预料之外的消息。使用WCF类库构建的客户端一般通过使用svcutil工具生成的代理对象,并使用该代理对象与服务端进行通信。代理对象提供了强类型的接口,该接口指定了客户端能够请求的操作。一个WCF客户端使用正确的代理对象不会发送预料之外的消息。但是,WCF服务是可以接受各种SOAP消息的,只要开发人员开发的客户端能发送这些SOAP消息。比如,Java开发人员开发的客户端使用Java的类库和工具构造和发送SOAP消息。WCF也提供了底层机制允许开发人员打开通道,创建SOAP消息,然后向服务端发送消息:
// Create a binding and endpoint to communicate with the ProductsService
BasicHttpBinding binding = new BasicHttpBinding();
EndpointAddress address = new EndpointAddress("http://localhost:8000/ProductsService/Service.svc");
ChannelFactory<IRequestChannel> factory = new ChannelFactory<IRequestChannel>(binding, address);
// Connect to the ProductsService service
IRequestChannel channel = factory.CreateChannel();
channel.Open();
// Send a ListProducts request to the service
Message request = Message.CreateMessage(MessageVersion.Soap11, "http://tempuri.org/IProductsService/ListProducts");
Message reply = channel.Request(request);
// Process the reply
// (should be a SOAP message with a list of product numbers)
...
// Release resources and close the connection
reply.Close();
channel.Close();
factory.Close();
你不用太关新上述代码片段的细节,因为你将在第十一章了解到更多消息和通道对象的细节。最主要是下面这行用以创建消息发送至服务的语法:
Message request = Message.CreateMessage(MessageVersion.Soap11, "http://tempuri.org/IProductsService/ListProducts");
方法CreateMessage的第二个参数指明了消息将被发送至服务某个行为。回想本章之前讲述用svcutil工具创建代理对象的细节,有一个WSDL文件,其内部也定义了上述的功能:
<wsdl:operation name="ListProducts">
<wsdl:input wsaw:Action="http://tempuri.org/IProductsServcie/ListProducts" message="tns:IProductsServcie_ListProducts_InputMessage" />
<wsdl:output wsaw:Action="http://tempuri.org/IProductsServcie/ListProductsResponse" message="tns:IProductsServcie_ListProducts_OutputMessage" />
<wsdl:fault wsaw:Action="http://tempuri.org/IProductsServcie/ListProductsDatabaseFaultFault" name="DatabaseFaultFault" message="tns:IProductsServcie_ListProducts_DatabaseFaultFault_FaultMessage" />
<wsdl:fault wsaw:Action="http://tempuri.org/IProductsServcie/ListProductsSystemFaultFault" name="SystemFaultFault" message="tns:IProductsServcie_ListProducts_SystemFaultFault_FaultMessage" />
</wsdl:operation>
当服务接收到通过指定行为是http://tempuri.org/IProductsServcie/ListProducts的消息时,服务执行ListProducts操作。如果一个客户端向服务端发送了一个未指定任何行为的消息,服务端将触发UnKnownMessageReceived事件。宿主程序将捕获该事件,并记录其为不可识别的消息:
private void btnStart_Click(object sender, RoutedEventArgs e)
{
try
{
productsServiceHost = new ServiceHost(typeof(Products.ProductsService));
productsServiceHost.Open();
// subscribe the faulted event
productsServiceHost.Faulted += (eventSender, eventArgs) =>
{
productsServiceHost.Abort();
productsServiceHost = new ServiceHost(typeof(Products.ProductsService));
productsServiceHost.Open();
};
productsServiceHost.UnknownMessageReceived +=(eventSendaer, eventArgs)
{
MessageBox.Show(string.Format("A client attempted to send the message {0}",
eventArgs.Message.Headers.Action));
};
...
}
catch (Exception ex)
{
handleException(ex);
}
}
对于客户端是否发送预料之外的消息;或者别用有心用户的为了掌控服务,不断向服务发起攻击以获取服务能支持的操作的详细信息。上述代码是能给出客户端是否清白的完美的诠释。
另外一种向服务端发送预料之外的消息可能是客户端的代理类已经过期了。如果开发人员修改了服务,那么他可能还修改了该服务所接收和发送的消息。如果客户端的程序没有更新,那么该客户端就可能向服务端发送服务端不识别的消息。因此,如果你更新服务,你应该确保你的服务能兼容旧版本的客户端。客户端向服务端发送预料之外的服务还有可能由数据契约引起,你将在第六章了解到如何安全的更新WCF服务的数据契约而不影响到客户端的正常调用。
总结
在本章,你了解了如何使用FaultException类,并使用它将异常的消息作为SOAP faults回传至客户端。你还了解了如何通过FaultContract特性来指定服务端能发送的Faults以及在客户端如何捕获这些Faults。你还了解了为了调试客户端如何从服务端向客户端推送预料之外的异常。你还理解了通过追踪服务的状态,如何将ServiceHost从faulted状态恢复。此外,通过处理客户端发送的预料之外的消息,从而构建一个"健壮"的宿主程序。
参考,本章内容较少,除了本书外,主要就是参考MSDN大本营。