WCF客户端运行时架构体系详解(上篇)
客户端调用WCF服务的方式不外乎有两种:其一、通过代码生成工具(比如SvcUtil.exe)导入服务的元数据生成服务代理相关的类型;其二、通过ChannelFactory<TChannel>创建服务代理对象。对于前者,生成的服务代理是一个继承自ClientBase<TChannel>的类型。对于这样一个服务代理对象,其内部本质上还是借助于ChannelFactory<TChannel>创建真正用于进行服务调用的代理对象。对于WCF客户端应用编程接口来说,ChannelFactory<TChannel>是一个核心类型。
一、创建ChannelFactory<TChannel>
服务调用的本质实际上是针对服务的某个终结点的调用,说得具体地应该是:客户端通过相匹配的终结点调用服务的终结点。终结点具有ABC(Address,
Binding, Contract)三要素,这里所说的“相匹配”的终结点具体体现在这三要素的匹配上。而服务调用最终体现在消息交换上,接下来我们从消息交换的角度来谈谈匹配终结点在服务调用的必要性。
地址(Address):地址作为调用服务的唯一标识并代表了服务所在的位置,客户端终结点必须具有一个正确的地址才能确保请求的消息被发送到正确的目的地;
绑定(Binding):作为信道层的缔造者,绑定最终创建了用于实现消息处理和传输的信道栈。客户端必须具有一个与服务端一致的信道栈,才能确保消息的一致性处理。具体来说,客户端必须具有与服务端一致的传输信道,才能确保消息能够被正常地传输到服务端。如果服务端具有采用一个基于HTTP协议的传输信道进行请求的监听,客户端就不能使用一个基于TCP的传输信道。服务端和客户端必须具有一个相同的消息编码信道才能确保被一方编码的消息能够被另一个解码。如果服务端采用基于文本的消息编码信道,客户端采用的消息编码信道就不能是基于二进制的。此外,几乎所有的WS-*规范在WCF的实现都是通过自定义信道来控制消息交换来完成的,所以这也要求客户端和服务端必须具有对等的信道设置;
契约(Contract):契约最终决定了基于某个操作的服务调用应该采用的消息交换模式,以及参与消息交换的消息本身所具有的结构。为了让客户端和服务端就此达成一致,必要要求双方采用等效的契约。
用于创建服务代理对象的ChannelFactory<TChannel>对象本身就是基于某个具体的客户端终结点创建的。你可以通过编程的方式(构造函数)指定终结点的三要素,也可以将此三要素定义在配置文件中,通过终结点配置名称(构造函数的endpointConfigurationName参数)来创建ChannelFactory<TChannel>。下面的代码片断给出了相关定义。
public abstract class ChannelFactory : CommunicationObject, IChannelFactory, ICommunicationObject, IDisposable { public ServiceEndpoint Endpoint { get; } } public class ChannelFactory<TChannel> : ChannelFactory, IChannelFactory<TChannel>, IChannelFactory, ICommunicationObject { //其他成员 public ChannelFactory(string endpointConfigurationName); protected ChannelFactory(Type channelType); public ChannelFactory(Binding binding, EndpointAddress remoteAddress); public ChannelFactory(Binding binding, string remoteAddress); public ChannelFactory(string endpointConfigurationName, EndpointAddress remoteAddress); } |
当我们通过调用构造函数创建某个ChannelFactory<TChannel>对象后,WCF会根据指定的终结点创建一个ServiceEndpoint对象。而该ServiceEndpoint就是ChannelFactory<TChannel>对象的核心,只读属性Endpoint返回的也就是这个ServiceEndpoint对象。ServiceEndpoint在ChannelFactory<TChannel>中的结构分布如下图所示。
二、客户端架构体系
WCF服务端架构体系的建立始于ServiceHost的开启,而整个架构体系根据创建ServiceHost时初始化的用于描述服务的ServiceDescription对象来构建的。与此类似,当我们开启ChannelFactory<TChannel>的时候,WCF会根据之前创建的ServiceEndpoint来构建客户端的运行时架构体系。
下图揭示了WCF客户端框架体系的大体结构。在该架构体系中,表示客户端运行时的ClientRuntime是其核心。当ChannelFactory<TChannel>开启的时候,Binding的BuildChannelFactory<TChannel>方法会被调用,其结果就是:调用所有绑定元素的同名方法,并将创建出来的信道工厂组合成信道工厂栈。而连接它和ClientRuntime的是一个名为ServiceChannelFactoryOverXxx的对象。根据由消息交换模式决定的信道形状(Channel
Shape)和是否支持会话,ServiceChannelFactoryOverXxx具体可分为6种:
ServiceChannelFactoryOverOutput/ServiceChannelFactoryOverOutputSession、
ServiceChannelFactoryOverRequest/ServiceChannelFactoryOverRequestSession、
ServiceChannelFactoryOverDuplex/ServiceChannelFactoryOverDuplex。
ClientRuntime是与DispatchRuntime相匹配的位于客户端的运行时,也是整个客户端框架体系的核心,以及我们正对客户端进行扩展频繁使用到的对象。接下来,我们也从扩展的角度来介绍客户端运行时。
信道初始化
ClientRuntime具有两个基于信道初始化器(ChannleInitializer)列表的属性,分别是ChannelInitializers和InteractiveChannelInitializers。
public sealed class ClientRuntime { //其他成员 public SynchronizedCollection<IChannelInitializer> ChannelInitializers { get; } public SynchronizedCollection<IInteractiveChannelInitializer> InteractiveChannelInitializers { get; } } |
其中ChannelInitializers中元素被称为ChannelInitializer,实现了IChannelInitializer接口。如下面的代码片断所示,IChannelInitializer具有唯一的Initialize方法用以对客户端信道(以IClientChannel对象表示)进行初始化。当客户端信道被创建之后,客户端运行时的每个ChannelInitializer的Initialize方法会被调用。
public interface IChannelInitializer { void Initialize(IClientChannel channel); } |
InteractiveChannelInitializers属性表示的信道初始化器被称为InteractiveChannelInitializer,实现了IInteractiveChannelInitializer接口。如下面的代码片断所示,该接口具有一组以异步模式定义的方法:BeginDisplayInitializationUI和EndDisplayInitializationUI。一般情况下,我们通过自定义InteractiveChannelInitializer提供一个指定客户端用户凭证的UI。
public interface IInteractiveChannelInitializer { IAsyncResult BeginDisplayInitializationUI(IClientChannel channel, AsyncCallback callback, object state); void EndDisplayInitializationUI(IAsyncResult result); } |
消息检验
对于服务端来说,当请求消息被反序列化之前,回复消息在序列化之后,它们会被分发给DispatchRuntime的DispatchMessageInspector列表以实现针对消息的后续处理。我们将这个机制成为“消息检验(Message
Inspection)”。消息检验机制同样应用于客户端。具体来说,ClientRuntime同样具有一组消息检验器,对应于它的只读属性MessageInspectors。
public sealed class ClientRuntime { //其他成员 public SynchronizedCollection<IClientMessageInspector> MessageInspectors { get; } } |
不过它们的名称为ClientMessageInspector,实现了具有如下定义的IClientMessageInspector接口。当被序列化后的请求消息被分发到信道层之前,接收到的回复消息被反序列化之后,都会被分发给ClientRuntime的ClientMessageInspector列表。在这两种情况下,BeforeSendRequest和AfterReceiveReply这两个方法分别被调用,实现针对于请求消息和回复消息的消息检验。
public interface IClientMessageInspector { void AfterReceiveReply(ref Message reply, object correlationState); object BeforeSendRequest(ref Message request, IClientChannel channel); } |
操作和操作选择
作为服务描述的OperationDescription对象在服务端运行时被转化成DispatchOperation对象,而客户端则被转化成一个ClientOperation对象。ClientRuntime的Operations属性包含一个ClientOperation的列表,用于表示定义在当前终结点契约的所有操作。
针对某个具体的服务调用,客户端必须针对当前的调用上下文从该操作列表中选择一个正确的ClientOperation对象。服务端运行时的操作选择机制可以实现在一个被称为DispatchOperationSelector的组件中。客户端也具有相似的操作选择机制,而操作选择器被称为ClientOperationSelector,实现了一个具有如下定义的IClientOperationSelector接口。具体的操作选择机制实现在SelectOperation方法中,传入的参数分别表示代表操作方法的MethodBase对象和传入的参数列表,而返回值表示最终选择的操所名称。具有布尔类型返回值的属性AreParametersRequiredForSelection则表示实施操作选择逻辑是否依赖于参数。
public interface IClientOperationSelector { string SelectOperation(MethodBase method, object[] parameters); bool AreParametersRequiredForSelection { get; } } |
客户端最终采用的操作选择器通过属性OperationSelector表示。除了Operations和OperationSelector属性之外,ClientRuntime还具有一个额外的属性UnhandledClientOperation。和DispatchRuntime的UnhandledDispatchOperation属性类似,此属性表示的ClientOperation并不存在于Operations属性表示的操作列表中。当操作选择器不能正确定找到相应的ClientOperation是,此属性表示的ClientOperation会被自动用于处理当前的服务调用。一般地,当你在定义服务契约的时候,将OperationContractAttribtue特性的Action定义成“*”时,ClientRuntime的UnhandledClientOperation属性就代表这个操作。Operations、OperationSelector和UnhandledClientOperation属性在ClientRuntime中的定义如下面的代码片断所示。
public sealed class ClientRuntime { //其他成员 public SynchronizedKeyedCollection<string, ClientOperation> Operations { get; } public IClientOperationSelector OperationSelector { get; set; } public ClientOperation UnhandledClientOperation { get; } } |
至此,我们对ClientRuntime具有的可扩展组件进行了全面的介绍,这些组件在ClientRuntime中的分布大致可以通过下图表示。
三、 客户端操作(ClientOperation)
代表客户端运行时的ClientRuntime的核心是一组代表定义在当前终结点契约中的所有操作的ClientOperation列表,我们很有必要对ClientOperation进行深入的了解。下面的代码片断列出了定义在ClientOperation的主要属性。
public sealed class ClientOperation { //其他成员 public string Name { get; } public string Action { get; } public string ReplyAction { get; } public bool IsOneWay { get; set; } public MethodInfo SyncMethod { get; set; } public MethodInfo BeginMethod { get; set; } public MethodInfo EndMethod { get; set; } public bool SerializeRequest { get; set; } public bool DeserializeReply { get; set; } public bool IsInitiating { get; set; } public bool IsTerminating { get; set; } public IClientMessageFormatter Formatter { get; set; } public SynchronizedCollection<FaultContractInfo> FaultContractInfos { get; } public SynchronizedCollection<IParameterInspector> ParameterInspectors { get; } } |
在定义服务契约的时候,我们通过应用OperationContractAttribute特性将定义在契约接口或类中的某个方法定义成服务操作。当我们针对某个终结点创建ChannelFactory<TChannel>的时候,反映操作描述的OperationDescription被创建出来。而当我们开启了ChannelFactory<TChannel>之后,OperationDescription对象被转变成真正的运行时操作对象ClientOperation。所以ClientOperation主要来源于OperationDescription,而最终决定于应用在操作方法上的OperationContractAttribute的定义。
首先,ClientOperaiton的Name、Action、ReplayAction和IsOneway对应于OperationContractAttribute特性的同名属性。而SyncMethod和BeginMethod/EndMethod则表示同步和异步调用时对应的MethodInfo对象。具体来说,当我们通过将应用在早最方法的OperationContractAttribute特性的AsyncPattern属性设置成true以定义异步模式的服务操作的情况下,BeginMethod/EndMethod属性对应于BeginXxx/EndXxx方法。关于具有异步模式的操作定义,请参阅《WCF技术剖析(卷1)》第4章《服务契约(Service
Contract)》。
布尔类型的属性SerializeRequest/DeserializeReply分别表示是否需要对请求消息进行序列化,以及对回复消息进行反序列化。如果操作仅仅具有一个唯一的类型为Message的参数,就无需对参数进行序列化。相应地,如果返回值(或者ref/out参数)也是一个唯一的Message对象,那么也无需对回复消息进行反序列化。另为一组布尔类型的属性IsInitiating/
IsTerminating对应于OperationContractAttribute特性的同名属性,表示在支持会话(Session)的情况下,相应的操作是否是用于初始化/终止会话的操作。
DispatchOperation使用DispatchMessageFormatter进行请求消息的反序列化和回复消息的序列化。与之类似,ClientOperation则采用ClientMessageFormatter进行请求消息的序列化和回复消息的反序列化。ClientMessageFormatter实现了一个具有如下定义的IClientMessageFormatter接口。上述的序列化和反序列化的操作分别实现在SerializeRequest和DeserializeReply方法中。而真正被使用的ClientMessageFormatter定义在ClientOpoeration的Formatter属性中。
public interface IClientMessageFormatter { object DeserializeReply(Message message, object[] parameters); Message SerializeRequest(MessageVersion messageVersion, object[] parameters); } |
而最后一个FaultContractInfos属性表述一个元素为FaultContractInfo的集合。该集合最终用于在出现异常时辅助实现针对错误消息(Fault
Message)的序列化和反序列化。
和DispatchOperation一样,ClientOperation具有一个ParameterInspectors属性表示一组参数检验器列表。DispatchOperation和ClientOperation的参数检验器实现了相同的接口IParameterInspector。我们可以自定义参数检器实现针服务调用前对输入参数的验证,以及服务调用后对返回值和输出参数的验证。
WCF客户端运行时架构体系详解[下篇]
当基于某个终结点创建的ChannelFactory<TChannel>被开启的之后,位于服务模型层的客户端运行时框架被成功构建。站在编程的角度看ChannelFactory<TChannel>,它就是一个创建用于服务调用的服务代理对象的工厂。由于服务调用需要借助于服务代理来完成,我们很有必要从整个客户端运行架构层面来了解服务代理和基于服务代理的服务调用是如何实现的。
一、服务代理是一个透明代理
如果你阅读了《WCF技术剖析(卷1)》第8章《客户端(Client)》,你应该知道通过ChannelFactory<TChannel>创建的服务代理对象是一个“透明代理(Transparent
Proxy)”对象。而这可以通过调用RemotingServices的静态方法IsTransparentProxy来检验。为此我写了如下一段简单的检验程序,而输出的结果证实了“服务代理是透明代理”的结论。
using (ChannelFactory<ICalculate> channelFactory = new ChannelFactory<ICalculate>("calculateservice")) { ICalculate calculator = channelFactory.CreateChannel(); bool isTransparentProxy = RemotingServices.IsTransparentProxy(calculator); Console.WriteLine("Service porxy is a transparent proxy? {0}.", isTransparentProxy ?"Yes" : "No"); } |
输出结果:
Service porxy is a transparent proxy? Yes. |
既然服务代理是一个透明代理,它一定对应了具体的真实代理(RealProxy)。实际上,服务代理对象内部具有一个类型为ServiceChannelProxy的对象作为其真实代理对象。ServiceChannelProxy是WCF中的一个继承自RealProxy的类型,而其核心则是一个类型为ServiceChannel的对象。ServiceChannelProxy和ServiceChannel均是定义在System.ServiceModel.Channels命名空间下的内部(Internal)类型。
当我们使用ChannelFactory<TChannel>创建一个服务代理的时候,WCF会根据代表客户端运行时的ClientRuntime创建一个ServiceChannel对象。并且调用之前创建的信道工厂栈并最终创建信道栈。由于ServiceChannel同时引用着代表服务模型层核心的ClientRuntime和信道层的信道栈,所以我们可以说ServiceChannel是连接WCF客户端服务模型层与信道层之间的纽带。当ServiceChannel被成功创建后,WCF会基于该对象创建ServiceChannelProxy对象。最然返回这个真实代理对象的透明代理。
当我们通过显式(将服务代理对象转换成ICommunicationObject类型,并显式调用其Open方法)或者隐式(如果服务代理在未开启的状态下被用于服务调用,在进行服务调用之前会被隐式地开启)开启时,整个信道栈会被开启。下图揭示了服务代理(透明代理)、ServiceChannelProxy(真实代理)、ServiceChannel、ClientRuntime和信道栈之间的关系。
二、服务调用的流程
由于服务代理是一个透明代理,所以针对它的任何一个方法调用都会最终转换到对其真实代理(ServiceChannelProxy)的Invoke方法的调用。所以ServiceChannelProxy会接管所有针对于服务代理对象的服务调用,并最终将调用递交给内部的ServiceChannel处理。
接下来,我们来简单地介绍一下针对一次简单的针对服务代理的服务调用,ServiceChannel在其内部是按照怎样的流程来处理的。实际上,相同的内容已经出现在了《WCF技术剖析(卷1)》第8章《客户端(Client)》中。下面的列表体现了ServiceChannel进行服务调用的整个流程(以请求/回复消息交换模式为例)。
操作选择
如果当前ClientRuntime的OperationSelector属性具有一个操作选择器,则调用其SelectOperation方法或者针对当前服务调用的客户端操作;
输入参数检验
遍历当前ClientRuntime的ParameterInspectors属性表示的参数检验器列表,调用其BeforeCall方法对输入参数实施检验;
序列化请求消息
通过当前ClientOperation的SerializeRequest属性判断是否需要进行请求消息的序列化。如果需要,则根据当前ClientOperation的Formatter属性获取消息格式化器,最终调用SerializeRequest方法将以方法调用形式体现的服务调用序列化成请求消息。
请求消息检验
遍历以当前ClientOperation的MessageInspectors属性表示的消息检验器列表,并调用BeforeSendRequest方法对请求消息实施发送前的检验。
请求消息的发送和回复消息的接收
将请求消息递交给信道层进行进一步处理,经过编码后的请求消息通过传输信道发送到服务端并等待回复。当回复消息抵达客户端后,信道层对其进行接收、解码相应的处理。
回复消息的检验
遍历以当前ClientOperation的MessageInspectors属性表示的消息检验器列表,并调用AfterReceiveReply方法对回复消息实施发送前的检验。
反序列化回复消息
通过当前ClientOperation的DeserializeReply属性判断是否需要进行回复消息的反序列化。如果需要,则根据当前ClientOperation的Formatter属性获取消息格式化器,最终调用DeserializeReply方法将包含在回复消息的调用结果反序列化成方法调用的返回值或者ref/out参数对象。
检验返回值(或者ref/out参数)
遍历当前ClientRuntime的ParameterInspectors属性表示的参数检验器列表,调用其AfterCall方法对返回值或者ref/out参数对象进行检验。
|