WCF ChannelFactory和通道 - 缓存、重用、关闭和恢复

12
我为我的WCF客户端库制定了以下的架构计划:
  • 使用ChannelFactory而不是svcutil生成的代理,因为我需要更多控制,并且我希望将客户端保留在单独的程序集中并避免在我的WCF服务更改时重新生成
  • 需要应用一个行为和消息检查器到我的WCF端点,以便每个通道能够发送自己的身份验证令牌
  • 我的客户端库将从MVC前端使用,因此我必须考虑可能存在的线程问题。
  • 我正在使用.NET 4.5(也许它有一些帮助程序或新方法来更好地实现WCF客户端?)

我读过关于各种不同部分的文章,但仍然对如何正确地组合它们感到困惑。我有以下问题:

  1. 据我所知,建议在静态变量中缓存ChannelFactory,然后从中获取通道,对吗?
  2. 端点行为是否特定于整个ChannelFactory,还是可以为每个通道单独应用我的身份验证行为?如果行为特定于整个工厂,则意味着我不能在我的端点行为对象中保留任何状态信息,因为相同的身份验证令牌将被重用于每个通道,但显然我希望每个通道都有其自己的身份验证令牌以供当前用户使用。这意味着我将不得不在我的端点行为中计算令牌(我可以将其保留在HttpContext中,而我的消息检查器行为只需将其添加到传出消息中)。
  3. 我的客户端类是可处理的(实现IDispose)。如何正确地处理通道的释放,因为它可能处于任何可能的状态中(未打开、已打开、失败……)?我只需释放它吗?我中止它然后释放?我关闭它(但它可能根本没有被打开),然后释放?
  4. 如果在使用通道时出现故障,我该怎么办?只有通道损坏了还是整个ChannelFactory都损坏了?

我想,一行代码胜过一千言语,所以以下是我的代码示例。我已经在上面标记了所有我的问题。

public class MyServiceClient : IDisposable
{
    // channel factory cache
    private static ChannelFactory<IMyService> _factory;
    private static object _lock = new object();

    private IMyService _client = null;
    private bool _isDisposed = false;

     /// <summary>
    /// Creates a channel for the service
    /// </summary>
    public MyServiceClient()
    {
        lock (_lock)
        {
            if (_factory == null)
            {
                // ... set up custom bindings here and get some config values

                var endpoint = new EndpointAddress(myServiceUrl);
                _factory = new ChannelFactory<IMyService>(binding, endpoint);

                // ???? do I add my auth behavior for entire ChannelFactory 
                // or I can apply it for individual channels when I create them?
            }
        }

        _client = _factory.CreateChannel();
    }

    public string MyMethod()
    {
        RequireClientInWorkingState();
        try
        {
            return _client.MyMethod();
        }
        catch
        {
            RecoverFromChannelFailure();
            throw;
        }
    }

    private void RequireClientInWorkingState()
    {
        if (_isDisposed)
            throw new InvalidOperationException("This client was disposed. Create a new one.");

        // ??? is it enough to check for CommunicationState.Opened && Created?
        if (state != CommunicationState.Created && state != CommunicationState.Opened)
            throw new InvalidOperationException("The client channel is not ready to work. Create a new one.");
    }

    private void RecoverFromChannelFailure()
    {
        // ??? is it the best way to check if there was a problem with the channel?
        if (((IChannel)_client).State != CommunicationState.Opened)
        {
            // ??? is it safe to call Abort? won't it throw?
            ((IChannel)_client).Abort();
        }

        // ??? and what about ChannelFactory? 
        // will it still be able to create channels or it also might be broken and must be thrown away? 
        // In that case, how do I clean up ChannelFactory correctly before creating a new one?
    }

    #region IDisposable

    public void Dispose()
    {    
        // ??? is it how to free the channel correctly?
        // I've heard, broken channels might throw when closing 
        // ??? what if it is not opened yet?
        // ??? what if it is in fault state?
        try
        {
            ((IChannel)_client).Close();
        }
        catch
        {
           ((IChannel)_client).Abort();              
        }

        ((IDisposable)_client).Dispose();

        _client = null;
        _isDisposed = true;
    }

    #endregion
}

我最终的实现方式几乎与上面的一样,看起来运行良好。我在RecoverFromChannelFailure中添加了一些代码来处理破损的工厂:lock (_lock){ if (_factory.State != CommunicationState.Opened) {_factory.Abort();_factory = null;}};并且我还有一个Initialize方法,它检查工厂是否已经消失,然后创建一个新的工厂。 - JustAMartin
关于身份验证,我最终采用了一个自定义的MessageInterceptorBehavior:IEndpointBehavior,IClientMessageInspector,IDispatchMessageInspector,该方法具有AfterReceiveRequest方法,WCF会在服务器和客户端端都调用它。 - JustAMartin
谢谢您的更新!处理破损的工厂是一个我可能会忘记的案例。顺便说一下,我在重用客户端通道方面遇到了一些问题:在跟踪中看到频繁但随机的TCP 995异常;这就是我问的原因。最终,我重用了工厂,但每次重新创建客户端通道都解决了我的问题。由于底层的TCP连接是池化的,所以似乎没有太大的成本,尽管我没有进行测量。 - henginy
1个回答

15

晚做总比不做好...看起来作者已经成功了,这可能有助于未来的WCF用户。

1)ChannelFactory安排通道,其中包括通道的所有行为。通过CreateChannel方法创建通道会“激活”该通道。通道工厂可以被缓存。

2)您可以使用绑定和行为来塑造通道工厂。这个形状与创建这个通道的每个人共享。正如您在评论中指出的那样,您可以附加消息检查器,但更常见的情况是使用Header将自定义状态信息发送到服务。您可以通过OperationContext.Current附加标头。

using (var op = new OperationContextScope((IContextChannel)proxy))
{
    var header = new MessageHeader<string>("Some State");
    var hout = header.GetUntypedHeader("message", "urn:someNamespace");
    OperationContext.Current.OutgoingMessageHeaders.Add(hout);
}

3) 这是我处理客户端通道和工厂的一般方法(此方法是我的ProxyBase类的一部分)

public virtual void Dispose()
{
    CloseChannel();
    CloseFactory();
}

protected void CloseChannel()
{
    if (((IChannel)_client).State == CommunicationState.Opened)
    {
        try
        {
            ((IChannel)_client).Close();
        }
        catch (TimeoutException /* timeout */)
        {
            // Handle the timeout exception
            ((IChannel)innerChannel).Abort();
        }
        catch (CommunicationException /* communicationException */)
        {
            // Handle the communication exception
            ((IChannel)_client).Abort();
        }
    }
}

protected void CloseFactory()
{
    if (Factory.State == CommunicationState.Opened)
    {
        try
        {
            Factory.Close();
        }
        catch (TimeoutException /* timeout */)
        {
            // Handle the timeout exception
            Factory.Abort();
        }
        catch (CommunicationException /* communicationException */)
        {
            // Handle the communication exception
            Factory.Abort();
        }
    }
}

4) WCF会故障通道而不是工厂。您可以实现重新连接逻辑,但这需要您从某个自定义的ProxyBase创建和派生客户端。

protected I Channel
{
    get
    {
        lock (_channelLock)
        {
            if (! object.Equals(innerChannel, default(I)))
            {
                ICommunicationObject channelObject = innerChannel as ICommunicationObject;
                if ((channelObject.State == CommunicationState.Faulted) || (channelObject.State == CommunicationState.Closed))
                {
                    // Channel is faulted or closing for some reason, attempt to recreate channel
                    innerChannel = default(I);
                }
            }

            if (object.Equals(innerChannel, default(I)))
            {
                Debug.Assert(Factory != null);
                innerChannel = Factory.CreateChannel();
                ((ICommunicationObject)innerChannel).Faulted += new EventHandler(Channel_Faulted);
            }
        }

        return innerChannel;
    }
}

5) 不要重复使用通道。打开、执行操作、关闭是正常的使用模式。

6) 创建共同的代理基类,并从中派生所有客户端。这可能会有所帮助,例如重新连接、使用预调用/后调用逻辑、消耗工厂事件(例如故障、开放)

7) 创建自己的CustomChannelFactory,这可以使您进一步控制工厂的行为,例如设置默认超时时间、强制执行各种绑定设置(MaxMessageSizes)等。

public static void SetTimeouts(Binding binding, TimeSpan? timeout = null, TimeSpan? debugTimeout = null)
        {
            if (timeout == null)
            {
                timeout = new TimeSpan(0, 0, 1, 0);
            }
            if (debugTimeout == null)
            {
                debugTimeout = new TimeSpan(0, 0, 10, 0);
            }
            if (Debugger.IsAttached)
            {
                binding.ReceiveTimeout = debugTimeout.Value;
                binding.SendTimeout = debugTimeout.Value;
            }
            else
            {
                binding.ReceiveTimeout = timeout.Value;
                binding.SendTimeout = timeout.Value;
            }
        }

@TCC 你尝试过其他的替代方案吗?你使用了良好的模式和实践吗? - Kiquenet
适用于_ServiceClient_吗?例如 MyUserServiceClient : System.ServiceModel.ClientBase<Portal.Admin.MobileServicesUsuario.IUsuarioService>, Portal.Admin.MobileServicesUsuario.IUsuarioService - Kiquenet
有没有使用良好模式和实践的代码示例,涉及到CustomChannelFactory通用代理基类重新连接逻辑以及ProxyBase类 - Kiquenet
很棒的答案!我知道这是一个旧帖子,但也许你仍然可以回答:ChannelFactory应该在与Channel同时被处理吗?您提到缓存工厂(以便在代理之间重用),因此最好有一些逻辑,允许仅在没有更多通道处于活动状态时才处理工厂。 - progLearner

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接