WCF的服务端架构体系又可以成为服务寄宿端架构体系。我们知道,对于一个基于某种类型的服务进行寄宿只需要使用到一个唯一的对象,那就是ServiceHost。甚至在某种语境下,我们所说的服务实际上就是指的对应的ServiceHost对象。整个服务寄宿过程包括两个阶段,即服务描述的创建和服务端运行框架的建立。而第一个阶段创建的服务描述是为了第二个阶段对服务端运行时框架建立服务的,所以我们有必要在对服务描述进行简单的介绍。
目录:
一、从服务描述(Service Description)谈起
二、服务端架构体系概览
三、终结点分发器选择机制
一、从服务描述(Service Description)谈起
当ServiceHost在被实例化的过程中,用于描述整个服务的ServiceDescription对象被创建出来。对于一个服务来说,它的核心包括:一组终结点列表和一组服务行为列表。这可以通过如下所示的ServiceDescription的定义看出来。
{
// 其他成员
public KeyedByTypeCollection < IServiceBehavior > Behaviors { get ; }
public ServiceEndpointCollection Endpoints { get ; }
}
而对于终结点来说,对于它的ABC三要素,即地址(Address)、绑定(Binding)和契约(Contract)早已了然于胸了。所以用于描述终结点的ServiceEndpoint类型具有Address、Binding和Contract三个核心属性。此外还有基于该终结点的行为列表,通过Behaviors属性表示。ServiceEndpoint的定义如下所示。
{
// 其他成员
public EndpointAddress Address { get ; set ; }
public Binding Binding { get ; set ; }
public ContractDescription Contract { get ; }
public KeyedByTypeCollection < IEndpointBehavior > Behaviors { get ; }
}
现在我们进一步分析用以描述服务契约的ContractDescription类型。由于服务契约本质上是一组相关操作的组合,所以ContractDescription的核心属性是如下所示的表示所有操作描述的Operations属性。除了操作描述列表之外,自然还有基于服务契约本身的行为列表。
{
// 其他成员
public OperationDescriptionCollection Operations { get ; }
public KeyedByTypeCollection < IContractBehavior > Behaviors { get ; }
}
至于对服务操作的描述,对应的类型为OperationDescription。OperationDescription中定义了一系列基于服务操作的属性,它们以及在之前的章节有过详细的介绍了,在这里我们主要关注的是用以表示操作行为列表的属性Behaviors。
{
// 其他成员
public KeyedByTypeCollection < IOperationBehavior > Behaviors { get ; }
}
上述从服务ServiceDescription到ServiceEndpoint,从ServiceEndpoint到ContractDescription,最终到OperationDescription的层次结构基本上可以通过下图来表示。
在构建ServiceHost过程中创建的用于描述整个服务的ServiceDescription对象,最终成为了构建服务端运行时架构体系的基础。而该架构体系在ServiceHost开启的过程中被构建出来,这也是为什么在ServiceHost开启之后对服务描述所作的任何该表都是无效的根本原因。
二、服务端架构体系概览
为了让读者对服务端运行时架构体系的结构具有更加深刻的认识,我们针对一个具体的服务寄宿应用场景来进行介绍。假设我们采用如下的配置对服务CalculatorService进行寄宿。通过这段配置,三个基于WSHttpBinding的终结点被添加。
< system.serviceModel >
< services >
< service name ="Artech.WcfServices.CalculatorService" >
< endpoint address = "http://127.0.0.1:7777/CalculatorService"
binding = "wsHttpBinding"
contract = "Artech.WcfServices.ICalculator"
listenUri = "http://127.0.0.1:6666/CalculatorService"
listenUriMode = "Explicit" />
< endpoint address = "http://127.0.0.1:8888/CalculatorService"
binding = "wsHttpBinding"
contract = "Artech.WcfServices.ICalculator"
listenUri = "http://127.0.0.1:6666/CalculatorService"
listenUriMode = "Explicit" />
< endpoint address = "http://127.0.0.1:9999/CalculatorService"
binding = "wsHttpBinding"
contract = "Artech.WcfServices.ICalculator"
listenUri = "http://127.0.0.1:6666/CalculatorService"
listenUriMode = "Unique" />
</ service >
</ services >
</ system.serviceModel >
</ configuration >
如上面的配置片断所示,虽然这三个终结点具有不同的地址,但是它们却使用了相同的监听URI(通过listenUri属性设置)。进一步地,虽然三个终结点具有相同的监听URI,但是它们的监听URI模式(通过listenUriMode属性设置),前两个终结点为Explicit,而第三个为Unique。按照在《WCF技术剖析(卷1)》第三章介绍的关于“物理地址和逻辑地址”原理,你会知道在这种情况下,最终的监听地址具有两个:http://127.0.0.1:6666/CalculatorService和http://127.0.0.1:6666/CalculatorService/<<guid>>。
当基于上面配置创建的ServiceHost在正常开启后,WCF会创建如下图所示的架构体系。首先通过调用绑定的BuildChannelListener方法创建信道监听器(实际上是多个信道监听器构成的信道监听器栈,最终返回的是最上层的信道监听器。如果读者对于信道层的相关内容不是特别了解,请参考《WCF技术剖析(卷1)》第3章《绑定与信道栈》)。这两个信道监听器分别绑定到上述的两个监听地址进行请求消息的监听。
针对这两个信道监听器,WCF会创建相应的信道分发器(ChannelDispatcher)对象。而针对在配置中定义的三个终结点,它们则分别对应着一个终结点分发器(EndpointDispatcher)。每个终结点分发器分发器都具有各自的运行时,被称为分发运行时(DispatchRuntime)。
当信道监听器成功监听到抵达的请求消息,它会利用创建的信道栈对消息进行接收和处理。经过信道栈处理过的消息通过信道监听器所在的信道分发器转发给相应的终结点分发器。终结点最终将接收到的消息在自己的分发运行时中进行处理。而处理后的结果被封装在创建的回复消息中回传给信道分发器,并最终通过信道栈返回给客户端。那么现在有一个问题:信道监听器在接收到经过信道栈接收和处理的消息后,如果判断需要将消息转发给哪个终结点分发器呢?这就是涉及到终结点分发器的选择机制。
三、终结点分发器选择机制
我们将注意力再次返回到上图。你会发现除了分发运行时,每个终结点分发器还具有两个重要的对象:地址筛选器(AddressFilter)和契约筛选器(ContractFilter)。它们都是属于一个叫做消息筛选器(MessageFilter)的对象。信道分发器就是通过这两个消息筛选器最终决定所在的终结点分发器是否适合处理当前请求消息。
具体来说,每个消息筛选器均继承自Dispatcher.MessageFilter这个抽象类。MessageFilter具有两个重载的分别以Message和MessageBuffer作为参数的方法。信道分发器在决定应该将接收的消息路由给哪个终结点分发器之前,会将基于路由消息的Message或者MessageBuffer对象作为输入参数,调用所有终结点分发器两个消息筛选器的Match方法。如果方法方法返回True,则表明该终结点分发器与需要路有的消息匹配。
{
public abstract bool Match(Message message);
public abstract bool Match(MessageBuffer buffer);
}
终结点分发器在WCF的应用编程接口中通过类型System.ServiceModel.Dispatcher.EndpointDispatcher表示。EndpointDispatcher的部分定义如下面的代码片断所示,除了代表上述两个消息筛选器的两个属性AddressFilter和ContractFilter之外,还有一个额外的整型的FilterPriority属性。FilterPriority属性表示筛选的优先级,当两个以上终结点分发器同时与路由的消息匹配的情况下,由优先级最高的终结点分发器会被选用。代表FilterPriority的数据越大,意味着优先级越高。如果同时有两个或者以上具有最高筛选优先级的终结终结点分发器,系统会抛出一个MultipleFilterMatchesException异常。
{
// 其他成员
public MessageFilter AddressFilter { get ; set ; }
public MessageFilter ContractFilter { get ; set ; }
public int FilterPriority { get ; set ; }
}
为了满足各种消息理由的需要,WCF为我们定义了如下六种典型的消息消息筛选器。如果这6种消息筛选器依然不能满足你的需求,你可以通过继承MessageFilter这个抽象类创建你自定义的消息筛选器。
- ActionMessageFilter:每一个服务操作具有一个Action属性,通过OperationContractAttribute特性进行定义。一个服务契约包含一个或者多个服务操作,所以一个终结点具有一组Action列表。AddressMessageFilter通过判断SOAP消息的Action报头的值是否在终结点Action列表之中,从而选择正确的终结点
- EndpointAddressMessageFilter:EndpointAddress是一个终结点不可或缺的元素,EndpointAddress不仅包含服务的地址,也包含寻址的报头(AddressHeader),能够通过EndpointAddressMessageFilter筛选的终结点需要同时满足两个要求:终结点地址URI需要与SOAP的To报头值一致;SOAP消息具一致的报头信息
- XPathMessageFilter:SOAP消息也是一个XML,所以可以根据一个具体的XPath表达式和SOAP的内容进行匹配
- PrefixEndpointAddressMessageFilter:和EndpointAddressMessageFilter筛选机制类似,不同的是PrefixEndpointAddressMessageFilter采用“最长前缀匹配”机制。比如,终结点地址指定的URI为http://www.artech.com/Foo,而请求消息的To报头的URI为http://www.artech.com/Foo/Bar,这样可以被认为是匹配的
- MatchAllMessageFilter:不管消息的内容是什么,都会匹配成功
- MatchNoneMessageFilter:和MatchAllMessageFilter相反,不管消息的内容是什么,都不会匹配成功
在默认的情况下,EndpointDispatcher的AddressFilter和ContractFilter分别采用的是EndpointAddressMessageFilter和ActionMessageFilter。如果希望使用其他的值,可以通过自定义Behavior的形式覆盖掉默认的值。对于AddressFilter,最直接的方式就是通过ServiceBehaviorAttribute的AddressFilterMode属性指定你所需要的MessageFilter模式。该属性的类型为AddressFilterMode枚举。它具有三个枚举值(Exact、Prefix和Any)对应于EndpointAddressMessageFilter、PrefixEndpointAddressMessageFilter和 MatchAllMessageFilter这三种消息筛选器。
{
// 其他成员
public AddressFilterMode AddressFilterMode { get ; set ; }
}
public enum AddressFilterMode
{
Exact,
Prefix,
Any
}
信道分发器对应的类型为ChannelDispatcher,下面的代码片断给出了ChannelDispatcher部分属性成员的定义。而这些属性代表了包含在信道分发器中那些可供扩展的组件。信道分发器是基于信道监听器创建的,后者用于请求消息的监听和消息接收信道栈的创建。信道监听器对应于只读属性Listener。
{
// 其他成员
public SynchronizedCollection < IChannelInitializer > ChannelInitializers { get ; }
public Collection < IErrorHandler > ErrorHandlers { get ; }
public ServiceThrottle ServiceThrottle { get ; set ; }
public override IChannelListener Listener { get ; }
}
ErrorHandler & ServiceThrottle
而属性ErrorHandlers代表的是一组ErrorHandler对象的集合。而ErrorHandler用于异常的处理的错误消息的提供。而类型为ServiceThrottle的同名属性用于进行流量控制,相关的内容你也可以参考《WCF中并发(Concurrency)与限流(Throttling)》。
ChannelInitializer
至于属性ChannelInitializers,则代表的是一组实现了接口System.ServiceModel.Dispatcher.IChannelInitializer的被称为信道初始化器的对象。顾名思义,所谓信道初始化器,就是当服务信道被创建之后用于对其进行初始化操作。接口IChannelInitializer的定义如下,它只具有一个唯一的Initialize方法。
{
void Initialize(IClientChannel channel);
}
为了实现自定义的异常处理和流量扩展等功能,你可以将自定义的相关组件应用到信道分发器中。另一方面,信道分发器本身具有一些用于控制器运行行为的属性。你也可以根据需要改变这些属性是信道分发器按照你希望的行为进行运作。下面的代码片断列出了信道分发器主要的可供修改的属性。其中通过属性MessageVersion表示的消息的版本(SOAP版本和WS-Addressing版本)决定于绑定的同名属性。
{
// 其他成员
public bool IncludeExceptionDetailInFaults { get ; set ; }
public bool ManualAddressing { get ; set ; }
public MessageVersion MessageVersion { get ; set ; }
public int MaxPendingReceives { get ; set ; }
public bool ReceiveSynchronously { get ; set ; }
public bool IsTransactedReceive { get ; set ; }
public int MaxTransactedBatchSize { get ; set ; }
public IsolationLevel TransactionIsolationLevel { get ; set ; }
public TimeSpan TransactionTimeout { get ; set ; }
}
IncludeExceptionDetailInFaults
IncludeExceptionDetailInFaults:表示服务端抛出的异常的详细信息是否需要通过错误消息回传给客户端。基于安全的需要,该属性的默认值为False。通常只有在调试的时候我们才需要让客户端得到服务端原始的错误信息,所以这个开关由服务行为ServiceDebugBehavior来控制。如下面的代码所示,ServiceDebugBehavior具有一个同名的属性。你也可以直接通过在服务类型上应用ServiceBehaviorAttribute特性通过命名属性控制这个开关。关于该属性背后的原理,你可以参考我的文章《ServiceDebugBehavior服务行为是如何实现异常的传播的?》
{
// 其他成员
public bool IncludeExceptionDetailInFaults { get ; set ; }
}
public sealed class ServiceBehaviorAttribute : Attribute, IServiceBehavior
{
// 其他成员
public bool IncludeExceptionDetailInFaults { get ; set ; }
}
ManualAddressing
而属性ManualAddressing则涉及到寻址(Addressing)的概念。对于一个支持WS-Addressing的SOAP消息来说,在其报头列表中包括一系列WS-Addressing报头(比如To、ReplyTo、RelatesTo等)以提供消息路由需要的寻址信息。在默认的情况下,这些寻址报头最终是通过位于信道栈最底层的传输信道(Transport Channel)来添加的。但是在某些情况下,我们希望手工地位消息添加相应的寻址报头,并希望该消息按照这些手工添加的寻址信息进行路由。我们将这种机制成为手工寻址(Manual Addressing)。
如果启用手工寻址,当消息最终通过传输信道向传输层发送的时候,传输信道会认为相应的寻址报头已经被成功添加,所以不会进行寻址报头的重复添加。ChannelDispatcher的ManualAddressing属性表示是否启用了手工寻址,其默认值决定于绑定的传输绑定元素的同名属性。按照寻址的需要,你可以在运行时动态变该属性值强制启用或者禁用手工寻址。
{
// 其他成员
public bool ManualAddressing { get ; set ; }
}
MaxPendingReceives
MaxPendingReceives表示允许的最大挂起(未处理)的消息数,默认值为1。该值可以通过终结点行为DispatcherSynchronizationBehavior来修改。如下所示,DispatcherSynchronizationBehavior具有一个同名的属性。
{
// 其他成员
public int MaxPendingReceives { get ; set ; }
}
ReceiveSynchronously
对于服务端信道层对请求消息的接收,到底采用同步还是异步的方式更加有效往往取决于具体采用的通信方式。在默认的情况下,对于同步/异步消息接收方式的选择取决于终结点的绑定。对于所有的系统预定义绑定类型,它们都实现了一个特殊的接口IBindingRuntimePreferences。如下面的代码片断所示,IBindingRuntimePreferences接口具有一个唯一的只读属性:ReceiveSynchronously。该属性就表示具体的绑定是否应该采用同步的消息接收方式。而在默认的情况下,绑定的ReceiveSynchronously属性值被作为对应的信道分发器的同名属性值。
{
bool ReceiveSynchronously { get ; }
}
对于几个我们常用的系统预定义绑定(BasicHttpBinding、WSHttpBinding、WSHttp2007Binding、WSDualHttpBinding、NetTcpBinding、NetNamedPipeBinding和NetMsmqBinding),除了NetMsmqBinding的ReceiveSynchronously属性可以是True外,其他绑定的该属性总是返回False。也就是说,除了NetMsmqBinding,其他的绑定总是以异步的方式进行消息的接收,这样可以及时地处理同时抵达的消息请求,并极大的改善服务的吞吐量。而对于NetMsmqBinding来说,它的ReceiveSynchronously属性和ExactlyOnce具有相同的值。
虽然在默认的情况下,绑定的ReceiveSynchronously属性决定了信道分发器的同名属性。但是你也可以通过扩展来改变该属性值。实际上,WCF为我们定义了一个类型为System.ServiceModel.Description.SynchronousReceiveBehavior的终结点行为来对信道分发器的ReceiveSynchronously属性值进行设定。当终结点应用了该行为之后,对应的信道分发器被自动设置为True,意味着采用同步的方式接收请求消息。
IsTransactedReceive & MaxTransactedBatchSize
接下来,我们来关于信道分发器与事务相关的几个属性。首先是基于事务的消息接收。为了判断某个绑定是否支持事务性消息接收,WCF定义了名称为System.ServiceModel.Channels.ITransactedBindingElement的接口。从下面给出的定义可以看出,ITransactedBindingElement具有唯一的只读属性TransactedReceiveEnabled,表明是否需要将消息的接收工作纳入到事务中进行。
{
bool TransactedReceiveEnabled { get ; }
}
从接口的名称我们就可以看出来,ITransactedBindingElement是为绑定元素定义的接口。对于一个具体的绑定来说,只要它的绑定元素列表中具有任何一个绑定元素实现了ITransactedBindingElement接口,并且TransactedReceiveEnabled属性返回True,就意味着这是一个基于事务性消息接收的绑定。而绑定的是否支持事务性消息接收在默认的情况下反应在信道分发器的IsTransactedReceive属性上,而另一个属性MaxTransactedBatchSize则表示允许纳入同一个事务进行的最大消息接收操作数。对于WCF预定义的所有绑定元素,只有基于MSMQ的两个绑定元素MsmqTransportBindingElement和MsmqIntegrationBindingElement实现了ITransactedBindingElement接口。
对于最初决定于绑定的这两个基于事务性消息接收的属性,我们也可以通过扩展对其进行动态修改以强制或者避免进行事务性消息接收。而对于MaxTransactedBatchSize属性的设定,WCF同样为我们定义了相应的终结点属性:System.ServiceModel.Description.TransactedBatchingBehavior。信道分发器的MaxTransactedBatchSize对应于TransactedBatchingBehavior的MaxBatchSize属性。
{
// 其他成员
public int MaxBatchSize { get ; set ; }
}
TransactionIsolationLevel & TransactionTimeout
如果你阅读看了我的文章《WCF事务编程([上篇]、[中篇]、[下篇])》你应该对信道分发器的另外两个基于事务的属性TransactionIsolationLevel和TransactionTimeout不会感到陌生。它们代表在事务的隔离级别和超时时限。这两个属性对应于我们熟悉的ServiceBehaviorAttribute特性的同名属性。
public sealed class ServiceBehaviorAttribute : Attribute, IServiceBehavior
{
// 其他成员
public IsolationLevel TransactionIsolationLevel { get ; set ; }
public string TransactionTimeout { get ; set ; }
}
作为WCF中一个核心概念,终结点在不同的语境中实际上指代不同的对象。站在服务描述的角度,我们所说的终结点实际上是指ServiceEndpoint对象。如果站在WCF服务端运行时框架来说,终结点实际上指代的是终结点分发器(EndpointDispatcher)。而ServiceEndpoint与EndpointDispatcher是一一匹配的,并且前者是创建后者的基础。而终结点分发器具有自己的运行,即分发运行时(DispatchRuntime)。
目录
一、终结点分发器(EndpointDispatcher)
二、分发运行时(DispatchRuntime)
可扩展组件
认证与授权
服务实例上下文
会话关闭通知
同步上下文
消息检验
操作与操作选择
可扩展属性
授权
审核
事务与会话
未处理操作
SOAP ValidateMustUnderstand处理
并发控制
一、终结点分发器(EndpointDispatcher)
除了之前介绍的三个辅助信道分发器向匹配的终结点分发器实施消息路由的三个属性(AddressFilter、ContractFilter和FilterPriority)之外,你还可以通过属性ContractName和ContractNamespace得到服务契约的名称和命名空间,以通过EndpointAddress属性得到相应的终结点地址。将消息路由到该终结点分发器的信道分发器可以通过属性ChannelDispatcher获得。但是对于终结点分发器来说,其重要的还是通过属性DispatchRuntime表示的分发运行时。
{
// 其他成员
public string ContractName { get ; }
public string ContractNamespace { get ; }
public MessageFilter AddressFilter { get ; set ; }
public MessageFilter ContractFilter { get ; set ; }
public int FilterPriority { get ; set ; }
public ChannelDispatcher ChannelDispatcher { get ; }
public DispatchRuntime DispatchRuntime { get ; }
public EndpointAddress EndpointAddress { get ; }
}
二、分发运行时(DispatchRuntime)
毫不夸张地说,终结点分发器的分发运行时是WCF整个服务端运行时架构体系的核心,同时也是对WCF服务端服务模型进行扩展重点考虑的对象。分发运行时之所以具有如此重要的地位,原因在于:终结点分发器接收到从信道分发器路由的消息的整个处理是在分发运行时中进行的。
和上面分析信道分发器一样,我们首先来看看分发运行时具有哪些可扩展的组件。终结点的分发运行时对应的类型为DispatchRuntime。下面的代码片断列出了这些扩展组件在DispatchRuntime中的对应的属性定义。
{
// 其他成员
public ServiceAuthorizationManager ServiceAuthorizationManager { get ; set ; }
public ServiceAuthenticationManager ServiceAuthenticationManager { get ; set ; }
public RoleProvider RoleProvider { get ; set ; }
public ReadOnlyCollection < IAuthorizationPolicy > ExternalAuthorizationPolicies { get ; set ; }
public IInstanceContextProvider InstanceContextProvider { get ; set ; }
public SynchronizedCollection < IInstanceContextInitializer > InstanceContextInitializers { get ; }
public InstanceContext SingletonInstanceContext { get ; set ; }
public IInstanceProvider InstanceProvider { get ; set ; }
public SynchronizedCollection < IInputSessionShutdown > InputSessionShutdownHandlers { get ; }
public SynchronizationContext SynchronizationContext { get ; set ; }
public SynchronizedCollection < IDispatchMessageInspector > MessageInspectors { get ; }
public SynchronizedKeyedCollection < string , DispatchOperation > Operations { get ; }
public IDispatchOperationSelector OperationSelector { get ; set ; }
}
1、认证与授权
分发运行时具有一组与安全相关的组件,具体来说是这组组件是与身份认证和授权相关。它们包括用于进行认证的ServiceAuthorizationManager,用于进行授权的ServiceAuthorizationManager,以及在在ASP.NET Roles安全主体权限模式下实现授权采用的RoleProvider和在自定义安全主体权限模式下自定义的授权策略(通过ExternalAuthorizationPolicies属性表示)。如果你阅读了《深入剖析授权在WCF中的实现[共14篇]》,相对对这四个对象不会感到陌生。
2、服务实例上下文
服务端框架对服务调用请求的处理最终必然体现在服务实例的创建和操作方法的调用。而服务实例并不是单独存储,而是存在于一个上下文中,该上下文被称为实例上下文(InstanceContext)。WCF服务端框架通过一个被称为实例上下文提供者(InstanceContextProvider)来提供基于当前服务请求对应的实例上下文。这里所说的实例上下文的提供机制包括两种情况下:创建新的服务上下文,或者提供一个现有之前创建好的实例上下文。
实例上下文通过类型InstanceContext表示,而所有的实例上下文提供者实现了一个具有如下定义的接口IInstanceContextProvider。WCF为我们提供了相应的实例上下文提供者以实现不同的实例上下文模式:单调(PerCall)、会话(PerSession)和单例(Single)。关于实例上下文、实例上下文模式以及它们最终采用怎样的实例上下文提供者,在《WCF技术剖析(卷1)》第9章《实例管理与会话》有详细的介绍。在这里你需要了解的是:WCF服务端框建最终使用的实例上下文提供者反映在InstanceContextProvider属性上。如果采用单例实例上下文模式,最终提供的实例上下文赋值给了属性SingletonInstanceContext。
{
InstanceContext GetExistingInstanceContext(Message message, IContextChannel channel);
void InitializeInstanceContext(InstanceContext instanceContext, Message message, IContextChannel channel);
bool IsIdle(InstanceContext instanceContext);
void NotifyIdle(InstanceContextIdleCallback callback, InstanceContext instanceContext);
}
{
void Initialize(InstanceContext instanceContext, Message message);
}
你可以将任意数量的实例上下文初始化器应用到WCF服务端分发系统中,而DispatchRuntime的只读属性InstanceContextInitializers代表了当前的实例上下文初始化器列表。当实例上下文被成功创建后,这些实例上下文初始化器将会以此被执行。
在默认的情况下,WCF会采用反射的方式调用服务类型的无参构造函数来创建服务实例。但是你可以通过自定义一个被称为实例提供者的组件来让WCF服务端分发系统按照你希望的方式创建你想要的实例作为最终的服务实例。所有的InstanceProvider都实现具有如下定义的接口System.ServiceModel.Dispatcher.IInstanceProvider,而DispatchRuntime的InstanceProvider属性表示这个真正被用于提供具体服务实例的实例提供者。《WCF技术剖析(卷1)》第9章《实例管理与会话》中也有关于实例提供者的介绍。
{
object GetInstance(InstanceContext instanceContext);
object GetInstance(InstanceContext instanceContext, Message message);
void ReleaseInstance(InstanceContext instanceContext, object instance);
}
3、会话关闭通知
在一个基于双工(Duplex)消息交换模式的会话中,如果客户端在完成了基于当前会话所有消息介绍工作时系统通知服务端以从事一些相关的处理工作,可以通过实现一个被称为输入会话关闭处理器(InputSessionShutdownHandler)的组件。该组件类型实现如下一个名为IInputSessionShutdown的接口。DoneReceiving方法会在接受到上述通知时被调用,而输入参数调用当前的双工信道。如果该信道出现错误(状态变成Faulted),方法ChannelFaulted会被调用。
{
void ChannelFaulted(IDuplexContextChannel channel);
void DoneReceiving(IDuplexContextChannel channel);
}
4、同步上下文
在默认的情况下,如果服务寄宿过程中的当前线程具有同步上下文(比如将Windows Forms应用作为服务的宿主,主线程具有一个类型为WindowsFormsSynchronizationContext的同步上下文),那么后续的处理将在该同步上下文中进行。而你可以通过DispatchRuntime的SynchronizationContext属性得到该同步上下文对象。
这就意味着,所有的处理均是按照同步的方式进行处理的,在高并发的情况下着往往是致命的。我们可以使用在服务类型上应用ServiceBehaviorAttribute特性并通过指定UseSynchronizationContext属性决定是否使用宿主线程当前的同步上下文。
public sealed class ServiceBehaviorAttribute : Attribute, IServiceBehavior
{
// 其他成员
public bool UseSynchronizationContext { get ; set ; }
}
5、消息检验
WCF允许你对服务端框架进行扩展以实现对路由道终结点分发器的消息进行后续的处理,我们把这个机制成为消息检验。比如说,如果说客户端希望向服务端传输一些与功能无关的上下文信息,可以将其封装成消息报头并添加到请求消息中。你就可以通过这个消息检验机制将上下文信息从相应的消息报头中获取出来。此外,通过消息的检验机制运行你对传入的消息进行相应的更改。如果后续处理中需要相应的控制信息,你可以将其通过该机制将这些信息以消息报头的方式至于传输的消息之中。
消息的检验机制通过自定义消息检验器(MessageInspector)。实际上WCF的客户端和服务端运行时具有自己的消息检验器;客户端的被称为客户端消息检验器(ClientMessageInspector),用于针对发出的请求消息和接收的回复消息进行检验;而服务端的被称为分发消息检验器(DispatchMessageInspector),用于针对于接收的请求消息和发送的回复消息进行检验。在这里我们主要讨论分发消息检验器,在介绍客户端运行时架构的时候我们会对客户端消息检验器进行相应的介绍。
分发消息检验器实现了接口IDispatchMessageInspector。从如下给出的IDispatchMessageInspector接口的定义中我们可以看出,该接口具有两个方法BeforeSendReply和AfterReceiveRequest。从方法名称就可以看出来,它们分别在请求消息被接收后以及回复消息被发送前对消息进行检验。
{
object AfterReceiveRequest( ref Message request, IClientChannel channel, InstanceContext instanceContext);
void BeforeSendReply( ref Message reply, object correlationState);
}
分发运行时具有一个分发消息检验器的列表,该列表通过属性MessageInspectors表示。根据具体应用中针对消息检查的需要,你可以在该列表中添加任意的分发消息检验器,它们按照添加的顺序依次执行。
6、操作与操作选择
我们说了,服务端分发体系对消息请求处理最终体现在多相应操作方法的执行。在服务表示中,操作通过类型OperationDescription表示。当服务端运行时框架通过服务描述被创建的时候,每一个OperationDescription会转变成DiaptchOperation对象。而DispatchRuntime的Operations属性就代表当前终结点的所有DispatchOperation集合。这是一个类似于字典的集合类型,而代表键值的字符串为操作的名称。
由于当前的分发运行时中大都具有多个DispatchOperation对象,而它接收的是一个消息,那么必须具有某种机制以实现根据接收的消息解析出对应的目标操作。这样一种操作的选择机制在WCF分发运行时中是通过一个被称为操作选择器(OperationSelector)的组件来实现的。操作选择器对应的接口为IDispatchOperationSelector,针对消息对操作的选择通过SelectOperation实现,方法的返回值代表操作的名称。当从该方法得到正确的操作名称,WCF就可以从Operations熟悉代表的操作列表中选择正确的DispatchOperation了。
{
string SelectOperation( ref Message message);
}
介绍了分发运行时可供扩展(添加或者替换)的组件之后,我们来看它具有哪些可以修改的属性,通过修改这些属性会对整个消息分发、实例上下文的激活以及服务操作的执行等行为具有怎样的影响。下面的代码片断列出了这些属性。
{
// 其他成员
public PrincipalPermissionMode PrincipalPermissionMode { get ; set ; }
public bool ImpersonateCallerForAllOperations { get ; set ; }
public AuditLogLocation SecurityAuditLogLocation { get ; set ; }
public bool SuppressAuditFailure { get ; set ; }
public AuditLevel ServiceAuthorizationAuditLevel { get ; set ; }
public AuditLevel MessageAuthenticationAuditLevel { get ; set ; }
public bool AutomaticInputSessionShutdown { get ; set ; }
public bool ReleaseServiceInstanceOnTransactionComplete { get ; set ; }
public bool TransactionAutoCompleteOnSessionClose { get ; set ; }
public bool IgnoreTransactionMessageProperty { get ; set ; }
public DispatchOperation UnhandledDispatchOperation { get ; set ; }
public bool ValidateMustUnderstand { get ; set ; }
public ConcurrencyMode ConcurrencyMode { get ; set ; }
}
7、授权
DispatchRuntime的属性PrincipalPermissionMode和ImpersonateCallerForAllOperations是与授权相关的两个属性。前者表示安全主体权限模式,后者表示是否以模拟的客户端Windows帐号执行所有的操作。它们对应于服务行为ServiceAuthorizationBehavior的同名属性。
{
// 其他成员
public bool ImpersonateCallerForAllOperations { get ; set ; }
public PrincipalPermissionMode PrincipalPermissionMode { get ; set ; }
}
8、审核
DispatchRuntime具有四个与审核相关的属性。其中SecurityAuditLogLocation决定了审核日志被入何处;SuppressAuditFailure决定了是否要抑制审核日志记录过程中出现的非关键异常的抛出;而ServiceAuthorizationAuditLevel和MessageAuthenticationAuditLevel则表示具体那些认证和授权相关的事件应该进行审核日志的记录。这四个属性对应于服务行为ServiceSecurityAuditBehavior的同名属性。
{
// 其他成员
public AuditLogLocation AuditLogLocation { get ; set ; }
public bool SuppressAuditFailure { get ; set ; }
public AuditLevel MessageAuthenticationAuditLevel { get ; set ; }
public AuditLevel ServiceAuthorizationAuditLevel { get ; set ; }
}
9、事务与会话
接下来介绍四个与会话和事务相关的属性。AutomaticInputSessionShutdown表示服务端是否在客户端关闭输出会话(Output Session)的时候是否关闭输入会话(Input Session)。关于会话的相关内容,在《WCF技术剖析(卷1)》第9章《实例管理与会话》中具有详细的介绍。至于这里指的输出和输入则是消息交换模式(MEP:Message Exchange Pattern),你可以从《WCF技术剖析(卷1)》的第4章《服务契约》中找到关于消息交换模式的详细介绍。
另外两个属性ReleaseServiceInstanceOnTransactionComplete和TransactionAutoCompleteOnSessionClose表示在事务提交之后是否自定释放服务实例,以及在会话关闭之后是否自动提交事务。上述的两个属性分别对应ServiceBehaviorAttribute的同名属性。而AutomaticInputSessionShutdown属性则定于ServiceBehaviorAttribute的AutomaticSessionShutdown属性。
public sealed class ServiceBehaviorAttribute : Attribute, IServiceBehavior
{
// 其他成员
public bool AutomaticSessionShutdown { get ; set ; }
public bool ReleaseServiceInstanceOnTransactionComplete { get ; set ; }
public bool TransactionAutoCompleteOnSessionClose { get ; set ; }
}
为了实现基于消息交换的事务传播,事务本身是被封装成一个TransactionMessageProperty对象并被消息属性的形式置于消息之中。而DispatchRuntime的IgnoreTransactionMessageProperty属性表示在接收到这么一消息的时候,是否要忽略其中的TransactionMessageProperty消息属性。
是否忽略消息中的IgnoreTransactionMessageProperty属性决定于终结点的两个要素,即绑定和契约。具体来说,如果绑定不支持事务流转(Transaction Flow),则该属性返回True。反之,还有需要分析服务契约中应用在操作上的TransactionFlowAttribute设置。相关的逻辑,请参阅本书第3章《事务》。
10、未处理操作
但我们在定义服务契约的时候,通过将OperationContractAttribute特性应用在相应的方法上使其成员一个服务操作。每个操作都具有一个Action属性,并最终决定了针对该操作的请求消息的Action报头的URI。我们可以通过OperationContractAttribute特性的同名属性设定操作的Action。如果该属性没有在OperationContractAttribute特性进行显式设置,对应的操作也具有一个默认值。当ServiceHost被开启之后,每一个终结点对应的操作都转换成DispatchOperation对象,并添加到DispatchRuntime的Operations属性表示的操作列表中。
而实际上我们可以通过OperationContractAttribute将操作的Action定义成“*”。这样的操作被称为为处理操作(Unhandled Operation)这样的操作最终同样会被转换成DispatchOperation对象,并作为DispatchRuntime的UnhandledDispatchOperation属性而存在。对于请求的消息,不能从DispatchRuntime的Operations属性表示的操作列表中找到一个相匹配的操作时,这个未处理操作会被用于选用。
11、SOAP ValidateMustUnderstand处理
DispatchRuntime的ValidateMustUnderstand属性用于指定是由系统还是由应用程序强制执行 SOAP MustUnderstand标头处理。使用该属性来关闭对到达的消息头强制执行验证。在正常执行过程中,将消息头与UnderstoodHeaders 属性进行比较,来确认是否由服务显式处理到达的消息。将此属性设置为false可以禁用此检查。当设置为false时,应用程序必须检查具有 MustUnderstand="true" 标记的标头,如果其中一个或多个标头没有被理解,则返回错误。当应用程序应接受任何传入的SOAP 消息(例如,使用类型化消息或非类型化消息)以及执行自定义标头处理时,这将很有用。该属性对应于ServiceBehaviorAttribute特性的同名属性。
public sealed class ServiceBehaviorAttribute : Attribute, IServiceBehavior
{
// 其他成员
public bool ValidateMustUnderstand { get ; set ; }
}
12、并发控制
最后一个属性ConcurrencyMode与并发有关,用于指定三种并发模式(Single、Reentrant和Multiple)中的某一种。它同样对应于ServiceBehaviorAttribute特性的同名属性。
public sealed class ServiceBehaviorAttribute : Attribute, IServiceBehavior
{
// 其他成员
public ConcurrencyMode ConcurrencyMode { get ; set ; }
}
终结点分发器在自己的运行时中对请求消息的处理最终肯定体现在相应操作的执行。如果从服务描述的角度来看,操作是一个OperationDescription对象。而服务端分发运行时中的操作则代表的是一个DispatchOperation对象。作为服务描述的一部分,服务所有终结点的所有操作描述(OperationDescription)在ServiceHost创建过程中被创建。而当ServiceHost被正常开始时,这些操作描述最终转换成分发操作(DispatchOperation)。而DispatchRuntime的Operations属性代表了对应终结点的所有分发操作。
目录:
一、序列化与反序列
二、调用上下文初始化
三、参数的检验
四、服务实例的释放
五、事务
六、操作的执行
七、参数和返回值的释放
八、身份模拟
总结
接下来,我们同样从可扩展的角度来分析DispatchOperation,下面的代码片断列出了所有可供扩展的属性。
{
// 序列化与反序列化
public bool DeserializeRequest { get ; set ; }
public bool SerializeReply { get ; set ; }
public SynchronizedCollection < FaultContractInfo > FaultContractInfos { get ; }
public IDispatchMessageFormatter Formatter { get ; set ; }
// 执行上下文初始化
public SynchronizedCollection < ICallContextInitializer > CallContextInitializers { get ; }
// 参数检验
public SynchronizedCollection < IParameterInspector > ParameterInspectors { get ; }
// 服务实例释放
public bool ReleaseInstanceAfterCall { get ; set ; }
public bool ReleaseInstanceBeforeCall { get ; set ; }
// 事务
public bool TransactionAutoComplete { get ; set ; }
public bool TransactionRequired { get ; set ; }
// 操作执行
public IOperationInvoker Invoker { get ; set ; }
// 参数/返回值的释放
public bool AutoDisposeParameters { get ; set ; }
// 身份模拟
public ImpersonationOption Impersonation { get ; set ; }
}
一、序列化与反序列
我们所示的服务操作的执行最终体现在执行服务实例的某个相应的操作方法。而调用方法需要传入参数,而参数是一个个实实在在的基于某种类型的对象。但是在这之前,所有的服务调用信息被封装在消息中(对应于Message对象)。那么,在真正执行方法调用之前首要的任务就是从请求消息中提取相应的信息并将其反序列化成方法的输入参数。在另一方面,当操作方法被正确执行后,执行的结果通过方法的返回值(或者ref/out参数)来体现。如果要将执行结果正确地回复给客户端,需要将它们进行序列化成消息。
如果你阅读了《WCF技术剖析(卷1)》第5章《序列化与数据契约》,你应该很清楚WCF通过一个被称为消息格式化器(MessageFormatter)组件来完成序列化和反序列化工作。对于服务端来说,这个消息格式化器被称为分发消息格式化器(DispatchMessageFormatter),它实现了一个具有如下定义的System.ServiceModel.Dispatcher.IDispatchMessageFormatter接口。两个方法分别用于对请求消息的反序列化和对回复消息的序列化。
{
void DeserializeRequest(Message message, object [] parameters);
Message SerializeReply(MessageVersion messageVersion, object [] parameters, object result);
}
此外,与序列化相关的还具有两个布尔类型的属性DeserializeRequest和SerializeReply。从语义上我们都知道,它们分别表示是否需要进行请求消息的反序列化和回复消息的序列化。只所有要为DispatchOperation定义如此两个属性,原因在于消息的序列化反序列化并不是在任何情况下都是需要的。比如,如果操作方法具有一个唯一的类型为Message的参数,那么对请求消息的反序列化是不需要的。同理,如果操作方法的返回值(并且没有ref/out参数)类型为Message,那么就不需要进行对回复消息的序列化。
上面介绍的都是基于正常服务调用情况下的序列化和反序列化。如果异常出现,我们可以针对定义在操作上的错误契约(Fault Contract)抛出FaultException<TDetail>异常。为了顺利完成针对该异常消息信息(TDetail类型对象),需要预先确定必要的错误契约相关的信息。这些辅助性信息被风转在一个FaultContractInfo对象中,而DispatchOperation的FaultContractInfos表示与该操作所有错误契约相关的FaultContractInfo集合。
二、调用上下文初始化
每个DispatchOperation都有一个以属性CallContextInitializers表示的调用上下文初始化器(CallContextInitializer)列表。每个CallContextInitializer都实现了具有如下定义的接口ICallContextInitializer。
{
void AfterInvoke( object correlationState);
object BeforeInvoke(InstanceContext instanceContext, IClientChannel channel, Message message);
}
当目标操作方法执行前后,列表中每个CallContextInitializer的BeforeInvoke和AfterInvoke方法分别被调用,用以实现针对操作方法执行环境的初始化和清理工作。其中BeforeInvoke的返回值会被传到AfterInvoke方法中作为输入参数(correlationState)。
举个例子,在《WCF技术剖析(卷1)》的第10章,我分别采用自定义ClientMessageInspector和CallContextInitializer实现了上下文信息从客户端到服务端的自动传播。其中ClientMessageInspector用于上下文信息的发送,而CallContextInitializer用于上下文信息的接收和设置。在接下来的部分,我们还将演示如何利用这两个组件(ClientMessageInspector和CallContextInitializer)实现另一个应用场景:让服务操作执行线程的语言文化与客户但保持一致。
三、参数的检验
参数的检验涉及到另一个重要的组件,即参数检验器(ParameterInspector),它实现了一个具有如下定义的接口IParameterInspector。每个DispatchOperation具有一个ParameterInspector列表,通过属性ParameterInspectors表示。
{
void AfterCall( string operationName, object [] outputs, object returnValue, object correlationState);
object BeforeCall( string operationName, object [] inputs);
}
我们通常通过自定义ParameterInspector的方式实现操作执行前对输入参数的检验,以及操作执行后对返回值/输出参数的检验。举个例子,微软的企业库(Enterprise Library)具有验证应用程序块(Validation Application Block)。这是一个非常不错的用于数据实体验证的框架,它允许你可以单独定义针对某个实体类型的验证策略。它提供了于WCF的集成,使我们可以将这些独立的验证策略通过声明或者配置的方式应用到相应的操作上,最终实现自动的参数验证。而最终完成验证的就是自定义的ParameterInspector。
四、服务实例的释放
包含有服务实例释放的时机涉及到DispatchOperation的两个属性ReleaseInstanceBeforeCall和ReleaseInstanceAfterCall。前者表明在服务操作之前释放现有的实例并创建新的实例。后者这表示服务实例会在服务操作执行后被释放。服务实例的释放通过调用InstanceContext的ReleaseServiceInstance方法,而该方法一般来说会在最终调用之前介绍的InstanceProvider的ReleaseInstance方法。
{
// 其他成员
public void ReleaseServiceInstance();
}
public interface IInstanceProvider
{
// 其他成员
void ReleaseInstance(InstanceContext instanceContext, object instance);
}
public sealed class OperationBehaviorAttribute : Attribute, IOperationBehavior
{
// 其他成员
public ReleaseInstanceMode ReleaseInstanceMode { get ; set ; }
}
public enum ReleaseInstanceMode
{
None,
BeforeCall,
AfterCall,
BeforeAndAfterCall
}
五、事务
DispatchOperation的两个布尔类型的属性TransactionRequired和TransactionAutoComplete是与事务相关。前者表示当前的操作是否应该在事务中执行,后者表示当操作执行之后是否自动提交事务。我们可以通过操作行为OperationBehaviorAttribute的TransactionScopeRequired和TransactionAutoComplete属性来控制它们。
public sealed class OperationBehaviorAttribute : Attribute, IOperationBehavior
{
// 其他成员
public bool TransactionAutoComplete { get ; set ; }
public bool TransactionScopeRequired { get ; set ; }
}
六、操作的执行
操作的执行最终落在了一个被称为操作执行器(OperationInvoker)的组件上。OperationInvoker实现了具有如下定义的IOperationInvoker接口。操作具有两种不同的执行方法,即同步和异步。通过操作执行实现在Invoke方法中,而InvokeBegin/InvokeEnd则用于实现异步执行。方法中的输入参数instance和inputs分别表示用于通过InstanceProvider提供的服务实例,以及通过MessageFormatter对请求消息进行反序列化生成的输入参数。而Invoke和InvokeEnd的返回值和输出参数(outputs)表示操作执行后得到的结果。
{
object [] AllocateInputs();
object Invoke( object instance, object [] inputs, out object [] outputs);
IAsyncResult InvokeBegin( object instance, object [] inputs, AsyncCallback callback, object state);
object InvokeEnd( object instance, out object [] outputs, IAsyncResult result);
bool IsSynchronous { get ; }
}
WCF提供两个内部(Internal)的OperationInvoker分别实现了操作的同步和异步实现,它们分别是SyncMethodInvoker和AsyncMethodInvoker。它们的实现原理其实很简单,就是采用反射的方式调用相应的操作方法。具体来说,SyncMethodInvoker和AsyncMethodInvoker通过调用操作描述(OperationDescription)的SyncMethod和BeginMethod/EndMethod实现了对操作的同步和异步调用。
{
// 其他成员
public MethodInfo BeginMethod { get ; set ; }
public MethodInfo EndMethod { get ; set ; }
public MethodInfo SyncMethod { get ; set ; }
}
七、参数和返回值的释放
当服务操作成功执行,并且执行的结果被序列化到回复消息中,无论是作为参数的对象还是作为返回值的对象都变成了“垃圾对象”。在正常的情况下,它们最终会被垃圾回收。但是,如果这些对象引用一些需要释放的资源,就有可能造成内存泄露。
我们应该很清楚,我们在设计这种类型的时候,一般会实现IDisposable接口,并将资源释放操作实现在Dispose方法中。而DispatchOperation的AutoDisposeParameters属性决定了对于实现了IDisposable接口的类型的参数和返回值,是否需要最终调用它们的Dispose方法。
我们可以通过操作行为OperationBehaviorAttribute的同名属性控制DispatchOperation的AutoDisposeParameters属性值。在默认的情况下,DispatchOperation的AutoDisposeParameters属性为True。如果你希望直接避免参数和返回值的释放操作,你可以通过该特性将属性设置为False。
public sealed class OperationBehaviorAttribute : Attribute, IOperationBehavior
{
// 其他成员
public bool AutoDisposeParameters { get ; set ; }
}
八、身份模拟
最后一个类型为ImpersonationOption类型的属性Impersonation在《模拟在WCF中的应用》已经详细介绍过了,用以表示是否在模拟客户端身份上下文中执行服务操作。它对应操作行为OperationBehaviorAttribute的同名属性。
public sealed class OperationBehaviorAttribute : Attribute, IOperationBehavior
{
// 其他成员
public ImpersonationOption Impersonation { get ; set ; }
}
总结
对于一个在分发运行时表示某个具体的服务操作的DispatchOperation对象来说,它的可扩展的核心组件包括CallContextInitializer、ParameterInspector、DispatchMessageFormatter和OperationInvoker。它们在DispatchOperation如下图所示。