WCF客户端运行时架构体系详解[上篇]
客户端调用WCF服务的方式不外乎有两种:其一、通过代码生成工具(比如SvcUtil.exe)导入服务的元数据生成服务代理相关的类型;其二、通过ChannelFactory<TChannel>创建服务代理对象。对于前者,生成的服务代理是一个继承自ClientBase<TChannel>的类型。对于这样一个服务代理对象,其内部本质上还是借助于ChannelFactory<TChannel>创建真正用于进行服务调用的代理对象。对于WCF客户端应用编程接口来说,ChannelFactory<TChannel>是一个核心类型。
目录
一、创建ChannelFactory<TChannel>
二、客户端架构体系
信道初始化
消息检验
操作和操作选择
三、 客户端操作(ClientOperation)
一、创建ChannelFactory<TChannel>
服务调用的本质实际上是针对服务的某个终结点的调用,说得具体地应该是:客户端通过相匹配的终结点调用服务的终结点。终结点具有ABC(Address, Binding, Contract)三要素,这里所说的“相匹配”的终结点具体体现在这三要素的匹配上。而服务调用最终体现在消息交换上,接下来我们从消息交换的角度来谈谈匹配终结点在服务调用的必要性。
- 地址(Address):地址作为调用服务的唯一标识并代表了服务所在的位置,客户端终结点必须具有一个正确的地址才能确保请求的消息被发送到正确的目的地;
- 绑定(Binding):作为信道层的缔造者,绑定最终创建了用于实现消息处理和传输的信道栈。客户端必须具有一个与服务端一致的信道栈,才能确保消息的一致性处理。具体来说,客户端必须具有与服务端一致的传输信道,才能确保消息能够被正常地传输到服务端。如果服务端具有采用一个基于HTTP协议的传输信道进行请求的监听,客户端就不能使用一个基于TCP的传输信道。服务端和客户端必须具有一个相同的消息编码信道才能确保被一方编码的消息能够被另一个解码。如果服务端采用基于文本的消息编码信道,客户端采用的消息编码信道就不能是基于二进制的。此外,几乎所有的WS-*规范在WCF的实现都是通过自定义信道来控制消息交换来完成的,所以这也要求客户端和服务端必须具有对等的信道设置;
- 契约(Contract):契约最终决定了基于某个操作的服务调用应该采用的消息交换模式,以及参与消息交换的消息本身所具有的结构。为了让客户端和服务端就此达成一致,必要要求双方采用等效的契约。
用于创建服务代理对象的ChannelFactory<TChannel>对象本身就是基于某个具体的客户端终结点创建的。你可以通过编程的方式(构造函数)指定终结点的三要素,也可以将此三要素定义在配置文件中,通过终结点配置名称(构造函数的endpointConfigurationName参数)来创建ChannelFactory<TChannel>。下面的代码片断给出了相关定义。
{
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 SynchronizedCollection<IChannelInitializer> ChannelInitializers { get; }
public SynchronizedCollection<IInteractiveChannelInitializer> InteractiveChannelInitializers { get; }
}
其中ChannelInitializers中元素被称为ChannelInitializer,实现了IChannelInitializer接口。如下面的代码片断所示,IChannelInitializer具有唯一的Initialize方法用以对客户端信道(以IClientChannel对象表示)进行初始化。当客户端信道被创建之后,客户端运行时的每个ChannelInitializer的Initialize方法会被调用。
{
void Initialize(IClientChannel channel);
}
InteractiveChannelInitializers属性表示的信道初始化器被称为InteractiveChannelInitializer,实现了IInteractiveChannelInitializer接口。如下面的代码片断所示,该接口具有一组以异步模式定义的方法:BeginDisplayInitializationUI和EndDisplayInitializationUI。一般情况下,我们通过自定义InteractiveChannelInitializer提供一个指定客户端用户凭证的UI。
{
IAsyncResult BeginDisplayInitializationUI(IClientChannel channel, AsyncCallback callback, object state);
void EndDisplayInitializationUI(IAsyncResult result);
}
消息检验
对于服务端来说,当请求消息被反序列化之前,回复消息在序列化之后,它们会被分发给DispatchRuntime的DispatchMessageInspector列表以实现针对消息的后续处理。我们将这个机制成为“消息检验(Message Inspection)”。消息检验机制同样应用于客户端。具体来说,ClientRuntime同样具有一组消息检验器,对应于它的只读属性MessageInspectors。
{
//其他成员
public SynchronizedCollection<IClientMessageInspector> MessageInspectors { get; }
}
不过它们的名称为ClientMessageInspector,实现了具有如下定义的IClientMessageInspector接口。当被序列化后的请求消息被分发到信道层之前,接收到的回复消息被反序列化之后,都会被分发给ClientRuntime的ClientMessageInspector列表。在这两种情况下,BeforeSendRequest和AfterReceiveReply这两个方法分别被调用,实现针对于请求消息和回复消息的消息检验。
{
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则表示实施操作选择逻辑是否依赖于参数。
{
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 SynchronizedKeyedCollection<string, ClientOperation> Operations { get; }
public IClientOperationSelector OperationSelector { get; set; }
public ClientOperation UnhandledClientOperation { get; }
}
至此,我们对ClientRuntime具有的可扩展组件进行了全面的介绍,这些组件在ClientRuntime中的分布大致可以通过下图表示。
代表客户端运行时的ClientRuntime的核心是一组代表定义在当前终结点契约中的所有操作的ClientOperation列表,我们很有必要对ClientOperation进行深入的了解。下面的代码片断列出了定义在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属性中。
{
object DeserializeReply(Message message, object[] parameters);
Message SerializeRequest(MessageVersion messageVersion, object[] parameters);
}
而最后一个FaultContractInfos属性表述一个元素为FaultContractInfo的集合。该集合最终用于在出现异常时辅助实现针对错误消息(Fault Message)的序列化和反序列化。
和DispatchOperation一样,ClientOperation具有一个ParameterInspectors属性表示一组参数检验器列表。DispatchOperation和ClientOperation的参数检验器实现了相同的接口IParameterInspector。我们可以自定义参数检器实现针服务调用前对输入参数的验证,以及服务调用后对返回值和输出参数的验证。