介绍
在.NET 3.0 SP1(与.NET 3.5一起发布) 中,WCF客户端创建有一个重要的性能改进。对BasicHttpBinding 来说,性能已经接近于创建ASMX代理。
ASMX 代理 vs WCF 代理
ASMX 代理比WCF代理更简单。前者是类型System.Web.Services.Protocols.SoapHttpClientProtocol的一个包装。在ASMX世界中,编程模型是两条平行线:
1) 没有ServiceContract的概念。客户端代理中的所有服务操作(WebMethod)的签名都是唯一的。客户端直接通过SoapHttpClientProcotol.Invoke来调用那些操作。所以在服务接口和底层消息处理系统上没有差别。
2) 没有明确的信道栈。SoapHttpClientProtocol在客户端包装了所有东西。它直接调用System.Net API HttpWebRequest.
相反WCF提供一种“垂直的”编程模型。你可以在客户端得到像服务端一样丰富的编程特性。WCF内部代理是一个包装在类型System.ServiceModel.Channels.ServiceChannel内部的.NET远程传输代理。传输代理允许你执行必要的类型转换并让你像使用ServiceContract接口那样使用代理调用服务操作。
创建WCF 代理的代价
正如我上篇博客指出的那样,有两种创建WCF客户端代理(信道)的常用方法:
A 使用WCF默认支持的ClientBase<T>,你可以使用svcutil.exe来生成客户端代理代码。
B 使用ChannelFactory<T>.CreateChannel().
ClientBase<T>类型化代理的常用模式是在每次调用过程中执行创建/释放资源:
// You may add try/catch block here ... CalculatorClient clientProxy = new CalculatorClient(); clientProxy.Open(); // Make a call with the proxy clientProxy.Add(1, 2); clientProxy.Close(); // Error handling code here ..
这里CalculatorClient是通过svcutil.exe自动生成的而且是ClientBase<ICalculator>的一个子类。
在.NET 3.0 中,创建/释放WCF客户端代理都是一个非常昂贵的操作。比ASMX代理要差很多。方案A在内部创建ChannelFactory<T>.每个代理都保留一个ChannelFactory作为私有字段。ChannelFactory的生命周期完全由代理控制。方案B 能稍微好一些因为你可以自己控制ChannelFactory<T>对象并能够实现只创建一次从而节省进一步创建代理的消耗。
为什么创建/释放ChannelFactory会如何昂贵?这是因为它涉及了以下几个主要操作:
1) 构造ContractDescription树
2) 反射所有需要的CLR类型
3) 构造信道栈
4) 释放所有资源
大多数情况下,这些数据在以相同方式创建的不同代理之间是相同的。
在.NET 3.0 SP1(与.NET 3.5 一起发布),在这部分有两个主要的性能改进:
1) ClientBase<T>内部对ChannelFactory进行缓存。这显著改进了方案A的性能。
2) 改进了信道管理逻辑并使方案A 和 B都能受益。
通过这些改进,创建WCF代理的性能与ASMX有相当可比性。
改进
改进ClientBase<T>性能的思路很简单:对ChannelFactory对象进行缓存。这与ASMX代理中的客户端类型缓存类似。
为ClientBase<T>进行的ChannelFactory缓存。
核心思想是在AppDomain层缓存ChannelFactory对象, 这样一个ChannelFactory对象的生存时间就不由代理控制了。缓存是一种最常使用(most recently used, MRU)缓存。缓存大小被硬编码为32以便于将最少使用的ChannelFactory对象从缓存中移出。
由于有了ChannelFactory 缓存,创建一个ClientBase<T>对象的粗略过程如下:
1) 在ClientBase<T>构造函数中,会有一个lookup 方法在缓存中寻找一个匹配的ChannelFactory.
2) 如果找到了,对这个ChannelFactory的引用数量会加1. 否则,会基于设置新创建一个ChannelFactory.
3) 在ClientBase<T>的内部信道(传输代理)创建之前,如果其他公共属性(比如ChannelFactory, Endpoint和ClientCredentials)被访问过,那么当前ClientBase<T>的缓存逻辑会被禁用。
4) 一旦内部信道被成功创建且ClientBase<T>的ChannelFactory对象没有被从缓存中去除而且缓存没有被禁用,ChannelFactory对象会被添加到缓存中。
什么是匹配的ChannelFactory?
我之前说过”匹配的“ChannelFactory了吗?是的,你得保证你可以基于ClientBase<T>构造函数中的输入设置来区分不同的ChannelFactory对象。有两种类型构造函数:将绑定作为参数的和不将绑定作为参数的。这里是那些不将绑定作为参数的:
ClientBase(); ClientBase(string endpointConfigurationName); ClientBase(string endpointConfigurationName, string remoteAddress); ClientBase(string endpointConfigurationName, EndpointAddress remoteAddress); ClientBase(InstanceContext callbackInstance); ClientBase(InstanceContext callbackInstance, string endpointConfigurationName); ClientBase(InstanceContext callbackInstance, string endpointConfigurationName, string remoteAddress); ClientBase(InstanceContext callbackInstance, string endpointConfigurationName, EndpointAddress remoteAddress);
对这些构造函数来说,所有的参数(包括默认的那个)都在下面列表中:
InstanceContext callbackInstance string endpointConfigurationName EndpointAddress remoteAddress
只要在构造ClientBase<T>的时候这些参数是一样的,我们可以完全假设会使用同样的ChannelFactory。幸运的是,String和EndpointAddress类型是不变的, 比如,我们可以做些简单的比较来确定这两个参数是否一样。对InstanceContext来说,我们可以使用对象引用比较。EndpointTrait<TChannel>类型作为MRU缓存的键。
这里是以绑定作为参数的两个构造函数:
ClientBase(Binding binding, EndpointAddress remoteAddress); ClientBase(InstanceContext callbackInstance, Binding binding, EndpointAddress remoteAddress);
既然绑定对象是不变的,比如,人们可以在一个代理上使用它,然后修改一下并在另外一个代理上使用,我们可以很容易地检查一个绑定对象是否有改动。因此当绑定被提供给构造函数时,对应代理的缓存机制被禁用。
禁用缓存
WCF支持当ChannelFactory被打开之前修改ChannelFactory和ContractDescription设置的扩展形式。对一个新创建的代理做如下改动会阻止它与其他代理共享同样的信道。ClientBase<T>的其他属性也是同样的逻辑:
ChannelFactory
Endpoint
ClientCredentials
当内部信道被创建且ClientBase<T>.Open被调用以后任何对ClientBase<T>属性的访问都会导致对应代理的缓存被禁用。这是很自然的,因为你也不想让一个代理重用为其他代理设置的不同契约或者安全设置的ChannelFactory。
信道管理
每个内部创建的信道都会被添加到System.ServiceModel.Channels.CommunicationObjectManager. 从这个类中添加/删除一个元素的性能已经在.NET 3.5 中改进。这是通过内部将List<T>换成Hashtable 实现的。这也是为什么方案2会有性能改进。
最佳实践
现在我得谈论一下内部实现了,我想总结一下如何使用WCF代理来实现最佳性能。
重用同样的代理
在很多情况,你会想重用同样的代理。因为这样会极大地提升性能。尤其是当你使用安全特性而对安全协议进行初始化很耗资源的情况下。
不要忘记在我在上一篇博客中提到的再使用代理之前要显式打开它。
使用代理来开启缓存
正如上面提到的,你可以使用ChannelFactory<T>.CreateChannel来创建代理或者使用自动生成的代理。如果你使用后者,你需要注意以下几点以使ChannelFactory被系统缓存:
1) 不要使用代理中将绑定作为参数的构造函数,你可以将绑定信息放到配置文件中去。
2) 不要访问代理的ChannelFactory、Endpoint 和 ClientCredentials的公共属性。
禁用缓存
如果你真的想禁用缓存,你可以简单地通过违反以上条件来实现。在一些场景,由于下面两个原因可能导致你不想让所有线程都使用同样的代理:
1) 信道的上下文不允许多线程访问。
2) 当使用一个单一代理时信道栈可能会有一些影响性能的瓶颈。
缓存的逻辑可以是非常简单,也可以是非常复杂。总之要记住以下几点:
1) 你需要为管理代理而实现正确的同步逻辑。
2) 你需要保证使用同样的代理。有时候,你可能想要为代理实现一个循环利用模式。
3) 你需要处理异常并重建缓存。
4) 缓存大小需要限制并且可以配置。
5) 你可能需要创建代理即便缓存池中没有可以使用的代理。