8

我一直在努力使用 WCF 代理。处置 WCF 代理的正确方法是什么?答案并非微不足道。

System.ServiceModel.ClientBase 违反了微软自己的 Dispose 模式

System.ServiceModel.ClientBase<TChannel> 确实实现IDisposable了,因此必须假定它应该在using-block 中处理或使用。这些是任何一次性用品的最佳做法。但是,该实现是明确的,因此必须将ClientBase实例显式转换为IDisposable,从而使问题变得模糊。

然而,最大的混乱来源是调用出错Dispose()ClientBase实例,甚至是因为它​​们从未打开过而出错的通道,都会导致抛出异常。这不可避免地意味着当堆栈展开时,解释错误的有意义的异常会立即丢失,using范围结束并Dispose()抛出一个无意义的异常,说明您无法处理有故障的通道。

上述行为是对dispose模式的诅咒,该模式指出对象必须能够容忍多次显式调用Dispose(). (请参阅http://msdn.microsoft.com/en-us/library/b1yfkh5e(v=vs.110).aspx,“ ...允许Dispose(bool)多次调用该方法。该方法可能选择不执行任何操作在第一次通话之后。 ”)

随着控制反转的出现,这种糟糕的实现成为一个真正的问题。IOC 容器(特别是 Ninject)检测IDisposable接口并Dispose()在注入范围结束时显式调用激活的实例。

解决方案:代理 ClientBase 并拦截对 Dispose() 的调用

ClientBase我的解决方案是通过子类化代理System.Runtime.Remoting.Proxies.RealProxy并劫持或拦截对Dispose(). 我的第一个替代品Dispose()是这样的:

if (_client.State == CommunicationState.Faulted) _client.Abort();
else ((IDisposable)_client).Dispose();

(请注意,这_client是对代理目标的类型化引用。)

NetTcpBinding 的问题

起初我以为这已经解决了,但后来我发现了生产中的一个问题:在某些极其难以重现的场景下,我发现使用 a 的通道NetTcpBinding在无故障的情况下没有正确关闭,即使Dispose正在被调用_client.

我有一个 ASP.NET MVC 应用程序,它使用我的代理实现连接到使用NetTcpBinding本地网络上的 WCF 服务,托管在只有一个节点的服务集群上的 Windows NT 服务中。当我对 MVC 应用程序进行负载测试时,WCF 服务(使用端口共享)上的某些端点会在一段时间后停止响应。

我努力重现这一点:在两台开发人员机器之间跨 LAN 运行的相同组件运行良好;一个控制台应用程序锤击真正的 WCF 端点(在暂存服务集群上运行),每个工作中有许多进程和许多线程;在登台服务器上配置 MVC 应用程序以连接到负载下工作的开发人员机器上的端点;在开发人员的机器上运行 MVC 应用程序并连接到暂存 WCF 端点有效。然而,最后一个方案只适用于 IIS Express,这是一个突破。在开发人员机器上的全脂 IIS 下对 MVC 应用程序进行负载测试时,端点将被占用,连接到暂存服务集群。

解决方案:关闭通道

在未能理解问题并阅读了许多 MSDN 页面和其他声称该问题根本不存在的资源之后,我尝试了一个远射并将我Dispose()的解决方法更改为...

if (_client.State == CommunicationState.Faulted) _client.Abort();
else if (_client.State == CommunicationState.Opened)
{
    ((IContextChannel)_client.Channel).Close();
    ((IDisposable)_client).Dispose();
}
else ((IDisposable)_client).Dispose();

...并且该问题在所有测试设置中以及在暂存环境中的负载下都不再发生!

为什么?

谁能解释可能发生了什么以及为什么Channel在调用之前明确关闭Dispose()解决了它?据我所知,这不应该是必要的。

最后,我回到开头的问题:Dispose WCF Proxy 的正确方法是什么?我的替代品是否Dispose()足够?

4

1 回答 1

0

据我所知,问题是调用Dispose处理了句柄,但实际上并没有关闭通道,然后通道保留资源,然后最终超时。

这就是为什么您的服务在负载测试期间一段时间后停止响应的原因:因为初始调用保留资源的时间比您想象的要长,并且后来的调用无法利用这些资源。

我想出了以下解决方案。解决方案的前提是调用Dispose应该足以处理掉句柄以及关闭通道。另一个好处是,如果客户端最终处于故障状态,则会重新创建它,以便后续调用成功。

如果ServiceClient<TService>通过依赖注入框架注入到另一个类中Ninject,那么所有资源都将被正确释放。

注意:请注意,在 的情况下Ninject,绑定必须定义一个范围,即它不能缺少一个InXyzScope或用一个定义InTransientScope。如果范围没有意义,则使用InCallScope.

这是我想出的:

public class ServiceClient<TService> : IDisposable
{
    private readonly ChannelFactory<TService> channelFactory;

    private readonly Func<TService> createChannel;

    private Lazy<TService> service;

    public ServiceClient(ChannelFactory<TService> channelFactory)
        : base()
    {
        this.channelFactory = channelFactory;

        this.createChannel = () =>
        {
            var channel = ChannelFactory.CreateChannel();

            return channel;
        };

        this.service = new Lazy<TService>(() => CreateChannel());
    }

    protected ChannelFactory<TService> ChannelFactory
    {
        get
        {
            return this.channelFactory;
        }
    }

    protected Func<TService, bool> IsChannelFaulted
    {
        get
        {
            return (service) =>
                {
                    var channel = service as ICommunicationObject;

                    if (channel == null)
                    {
                        return false;
                    }

                    return channel.State == CommunicationState.Faulted;
                };
        }
    }

    protected Func<TService> CreateChannel
    {
        get
        {
            return this.createChannel;
        }
    }

    protected Action<TService> DisposeChannel
    {
        get
        {
            return (service) =>
                {
                    var channel = service as ICommunicationObject;

                    if (channel != null)
                    {
                        switch (channel.State)
                        {
                            case CommunicationState.Faulted:
                                channel.Abort();
                                break;

                            case CommunicationState.Closed:
                                break;

                            default:
                                try
                                {
                                    channel.Close();
                                }
                                catch (CommunicationException)
                                {
                                }
                                catch (TimeoutException)
                                {
                                }
                                finally
                                {
                                    if (channel.State != CommunicationState.Closed)
                                    {
                                        channel.Abort();
                                    }
                                }
                                break;
                        }
                    }
                };
        }
    }

    protected Action<ChannelFactory<TService>> DisposeChannelFactory
    {
        get
        {
            return (channelFactory) =>
                {
                    var disposable = channelFactory as IDisposable;

                    if (disposable != null)
                    {
                        disposable.Dispose();
                    }
                };
        }
    }

    public void Dispose()
    {
        DisposeChannel(this.service.Value);
        DisposeChannelFactory(this.channelFactory);
    }

    public TService Service
    {
        get
        {
            if (this.service.IsValueCreated && IsChannelFaulted(this.service.Value))
            {
                DisposeChannel(this.service.Value);

                this.service = new Lazy<TService>(() => CreateChannel());
            }

            return this.service.Value;
        }
    }
}
于 2015-08-14T08:51:45.147 回答