【问题标题】:Best Practice for WCF proxy lifetime - or how often to close a WCF proxy?WCF 代理生命周期的最佳实践 - 或多久关闭一次 WCF 代理?
【发布时间】:2010-10-28 22:03:48
【问题描述】:

我一直在开发一个使用 WCF 访问服务器端逻辑和数据库的 WPF 应用程序。

我从一个 WCF 客户端代理对象开始,我反复使用它来调用服务器上的方法。使用代理一段时间后,服务器最终会抛出异常: System.ServiceModel.EndpointNotFoundException: There was no endpoint listening at http://.../Service/BillingService.svc that could accept the message. This is often caused by an incorrect address or SOAP action. See InnerException, if present, for more details. ---> System.Net.WebException: Unable to connect to the remote server ---> System.Net.Sockets.SocketException: An operation on a socket could not be performed because the system lacked sufficient buffer space or because a queue was full.

我认为这是因为每个服务调用都打开了一个从代理到服务器的新套接字,而从未关闭它们。最终服务器被淹没并开始拒绝请求。

经过短暂的搜索后,我确定我需要定期关闭()代理。我发现的样本非常小。 This one 提供了一些有用的提示,但并没有真正回答问题。我还 seen recommendations 避免使用 using() 模式(并应用 try/catch/finally),因为代理的 Dispose 方法可能会引发异常(糟糕)。

似乎推荐的模式是这样形成的:

[TestClass]
public class WCFClientUnitTest
{
    BillingServiceClient _service;

    [TestMethod]
    public void TestGetAddressModel()
    {
        List<CustomerModel> customers = null;

        try
        {
            _service = new BillingServiceClient();
            customers = _service.GetCustomers().ToList();
        }
        catch
        {
            _service.Abort();
            _service = null;
            throw;
        }
        finally
        {
            if ((_service != null) &&
                (_service.State == System.ServiceModel.CommunicationState.Opened))
                _service.Close();
            _service = null;
        }

        if (customers != null)
            foreach (CustomerModel customer in customers)
            {
                try
                {
                    _service = new BillingServiceClient();
                    AddressModel address = (AddressModel)_service.GetAddressModel(customer.CustomerID);

                    Assert.IsNotNull(address, "GetAddressModel returned null");
                }
                catch
                {
                    _service.Abort();
                    _service = null;
                    throw;
                }
                finally
                {
                    if ((_service != null) &&
                        (_service.State == System.ServiceModel.CommunicationState.Opened))
                        _service.Close();
                        _service = null;
                }
            }
    }

所以我的问题仍然围绕着我应该让客户端代理保持多长时间?我应该为每个服务请求打开/关闭它吗?这对我来说似乎太过分了。我不会对性能造成重大影响吗?

我真正想做的是创建并打开一个通道,并在通道上进行短暂的重复、短、连续的服务调用。然后很好地关闭频道。

附带说明,虽然我还没有实现它,但我很快就会向服务(SSL 和 ACL)添加一个安全模型,以限制谁可以调用服务方法。 this post 的答案之一提到重新协商身份验证和安全上下文会使为每个服务调用重新打开通道很浪费,但只是建议避免构建安全上下文。


编辑 2010 年 11 月 3 日:这似乎很重要,所以我将其添加到问题中......

作为对Andrew Shepherd's 评论/建议的回应,我在关闭 TrendMicro AntiVirus 的情况下重新运行了单元测试,同时监视 netstat -b 的输出。 Netstat 能够记录 WebDev.WebServer40.exe 拥有的开放端口的显着增长。绝大多数端口处于 TIME_WAIT 状态。 Microsoft says 客户端关闭连接后端口可能会在 NET_WAIT 中徘徊...

注意:有插座是正常的 TIME_WAIT 状态长时间 的时间。时间在 RFC793 作为最大段的两倍 终生 (MSL)。 MSL 被指定为 2分钟。所以,一个套接字可能在一个 TIME_WAIT状态长达4 分钟。一些系统实现 不同的值(少于 2 分钟) 为 MSL。

这让我相信,如果每个服务调用都在服务器上打开一个新套接字,并且因为我在一个紧密的循环中调用服务,我很容易淹没服务器,导致它用完可用的套接字并进入反过来生成我上面提到的异常。

因此,我需要走以下两条路之一: 1)尝试批处理服务调用,以便它们重用服务器端套接字 2) 更改我的服务合同,以便我可以用更少的调用返回更大的数据块。

第一选择对我来说似乎更好,我将继续追求它。我会发回我的发现并欢迎更多的 cmets、问题和答案。

【问题讨论】:

  • “每个服务调用都打开了一个从代理到服务器的新套接字,并且从不关闭它们”:这真的是真的吗?
  • 也许我说“永远不要关闭它们”有点强硬。如何测量应用程序打开的套接字?我现在正在运行性能监视器,以查看是否可以收集一些具体数据。我读了另一篇提到netstat的帖子。我以前没用过netstat。
  • 嗯,性能监视器没有显示很多有用的东西。但是 netstat 能够在我的单元测试运行期间随着时间的推移在开放套接字中产生显着增长。单元测试完成后大约一分钟,套接字关闭。奇怪的是,netstat -b 将大多数套接字所有者归因于我的 TrendMicro 防病毒软件。
  • 如果您暂时禁用防病毒软件会怎样?

标签: wcf proxy


【解决方案1】:

(发布完全不同的第二个答案)

如果我们谈论“最佳实践”,WCF 的真正最佳实践是拥有“粗粒度方法”。如果一个客户端在一个循环中多次调用一个方法,那么整个业务逻辑应该转移到服务本身。

例如。

[DataContract]
class CustomerAndAddress
{
    [DataMember]
    CustomerModel Customer;

    [DataMember]
    AddressModel Address;
}

[ServiceContract]
class BillingService
{
   [OperationContract]
   CustomerAndAddress[] GetAllCustomersAndAddresses();
}

或者,在现实世界中更有可能:

[ServiceContract]
class BillingService
{
   [OperationContract]
   CustomerReportData FetchCustomerReportInfo(CustomerReportParameters parameterSet);
}

话虽如此,我仍然有兴趣看看你是否能完成你正在尝试的事情。

【讨论】:

  • 我最初是从与此类似的设计开始的,但是即使在将 maxBufferPoolSize、maxBufferSize、maxReceivedMessageSize 和 maxItemsInObjectGraph 增加到非常大的数字之后,有效负载也会耗尽传输缓冲区。我重写了界面,从几个大请求变成了许多小请求。
  • 我要问的下一个问题是:为什么客户端需要这么多细粒度的数据?您不希望在屏幕上显示成千上万的客户,因为用户一次只能看到少数几个。如果客户端以某种方式聚合此数据,则聚合可以由服务执行,该服务将返回聚合值。
  • 为什么问题会很困难,除了说这是业务需求。更详细地说,我展示了大约 100 个客户。用户为客户选择一个客户和一张发票,两者都可以正常工作。当客户有一张包含 1000 行的大发票时,就会出现问题。会计部门需要查看完整的发票,并最终将其打印或导出到 Excel。
  • 此外,我希望用户可以全天(甚至可能整周)运行此应用程序。因此,我试图了解围绕 WCF 服务代理的生命周期所涉及的权衡以及如何有效地管理资源。
  • 安德鲁,尽管这不是我正在寻找的答案,但我会给你 +1 以帮助我解决问题。感谢您的帮助。
【解决方案2】:

这里似乎有几件事在起作用。

首先,我使用 netstat –b 运行和监控的单元测试指出 Cassini (WebDev.WebServer40.exe) 是在 TIME_WAIT 状态下累积的端口的所有者。正如引用的 MSFT kb 文章所指出的,在 FIN 握手之后端口徘徊是正常的,而应用程序等待网络上的任何慢速数据包被传递并且消息队列耗尽。 2 分钟的默认配置解释了为什么我在单元测试完成后看到端口装病。虽然可以通过注册表设置更改 MSL,但不建议这样做。

但是,我几乎忽略的重要一点是该服务是在 Cassini 下运行的。当我将服务器端点切换到在 IIS7 下运行时,我根本没有遇到任何端口增长!我无法解释这是否意味着客户端正在重用端口,或者 IIS7 在完成端口清理后是否比 Cassini 更好。 现在,这并不能完全回答我应该多久关闭一次 WCF 代理的问题。这只是意味着我没有经常关闭代理。

我可以看到,要让代理长时间保持打开状态,仍然需要进行资源权衡。

如果您有许多(即数千个)客户端访问您的 WCF 服务,则在调用之间或小批量调用之间释放服务器资源可能是有意义的。在这种情况下,请务必使用 try/catch/finally 机制而不是 using(),因为即使服务代理实现了 IDisposable,如果服务处于故障状态,close() 方法也会抛出异常。

另一方面(在我的特殊情况下),如果您只希望有几个客户端访问您的 WCF 服务,那么您不需要频繁且显式地打开和关闭服务代理的额外复杂性。因此,我打算在我的应用程序启动时打开一次代理,并保持打开状态直到应用程序完成。我打算实现一个服务调用辅助方法(类似于:Renewing a WCF client when SCT has expired?),它将回收连接,以防万一它进入故障状态。然后,我就不必担心管理代理的生命周期了。

如果认为我误读了我的测试结果,或者您有更好的解决方案,请告诉我。

【讨论】:

  • 同意,我在混合端口和套接字方面马虎/懒惰。我希望你能明白。
【解决方案3】:

我写了(好吧,找到并修改了)一个包装器来帮助正确地处理服务。对不起VB。它里面有一些额外的东西,比如MessageViewerInspector,所以我可以拦截进出服务的 XML。暂时忽略它。

编辑:我知道这并不能真正回答您关于如何维护请求突发的问题,但它肯定会使使用该服务更简洁、更符合代码。

Imports System.ServiceModel

Namespace WCF

    ''' <summary>
    ''' This helper fixes an issue with WCF services where an exception gets thrown
    ''' during the Dispose() call of the client, so it doesn't actually get disposed.
    ''' </summary>
    ''' <typeparam name="TProxy"></typeparam>
    ''' <typeparam name="TChannel"></typeparam>
    ''' <remarks></remarks>
    Public Class ServiceProxyHelper(Of TProxy As {ClientBase(Of TChannel), New}, TChannel As Class)
        Implements IDisposable

        Private _proxy As TProxy
        Private _inspector As MessageViewerInspector

        Public ReadOnly Property ServiceProxy() As TProxy
            Get
                If Not _proxy Is Nothing Then
                    Return _proxy
                Else
                    Throw New ObjectDisposedException("ServiceProxyHelper")
                End If
            End Get
        End Property

        Public ReadOnly Property Inspector() As MessageViewerInspector
            Get
                If _inspector Is Nothing Then
                    _inspector = New MessageViewerInspector()
                End If
                Return _inspector
            End Get
        End Property

        Public Sub New()
            _proxy = New TProxy()
            _proxy.Endpoint.Behaviors.Add(Me.Inspector)
        End Sub

        Public Sub New(ByVal endpointAddress As String)
            Me.New()
            If Not Me._proxy Is Nothing AndAlso Not String.IsNullOrEmpty(endpointAddress) Then
                Me._proxy.Endpoint.Address = New EndpointAddress(endpointAddress)
            End If
        End Sub

        Public Sub Dispose() Implements IDisposable.Dispose

            Try

                If Not _proxy Is Nothing Then
                    If _proxy.State <> CommunicationState.Faulted Then
                        _proxy.Close()
                    Else
                        _proxy.Abort()
                    End If
                End If

            Catch ex As CommunicationException

                _proxy.Abort()

            Catch ex As TimeoutException

                _proxy.Abort()

            Catch ex As Exception

                _proxy.Abort()
                Throw

            Finally

                _proxy = Nothing

            End Try

        End Sub

    End Class

End Namespace

那么你可以这样使用它:

    Using myService As New ServiceProxyHelper(Of MyService.MyServiceClient, MyService.IMyService)
        Try
            ' Do work
            myService.ServiceProxy.DoWork()
        Catch ex As FaultException(Of MyService.MyServiceException)
            ' Log exception
        End Try
    End Using

【讨论】:

  • 感谢 Cory,这是一个很好的帮助类,用于管理您希望在每次服务调用之间终止代理并破解 ClientBase 中的 Dispose 错误以便您可以使用 using() 的情况。注意 FWIW,Dispose Catch 块的行为都相同,可以合并到单个 Catch/_proxy.Abort()/Throw...
  • 注意If Not _proxy Is Nothing Then ... Else _proxy.Abort() End If 没有意义。如果代理为空/无,则不能中止它。我知道这是一篇旧帖子,我只是想指出这一点。
  • @p.s.w.g:很好。我认为我搞砸了那里的嵌套 if 语句。我会解决的。
【解决方案4】:

您已经在问题中给出了自己的答案。

“我真正想做的是创建并打开一个通道,并在通道上进行短暂的重复、短暂、连续的服务调用。然后很好地关闭通道”

没错。

将此应用于您给出的示例,您可以将其简化为:

    try
    {
        _service = new BillingServiceClient();
        customers = _service.GetCustomers().ToList();

    if (customers != null)
        foreach (CustomerModel customer in customers)
        {
                AddressModel address = (AddressModel)_service.GetAddressModel(customer.CustomerID);

                Assert.IsNotNull(address, "GetAddressModel returned null");
        }

    }
    catch
    {
          _service.Abort();
          _service = null;
          throw;
     }
     finally
     {
           if ((_service != null) &&
             (_service.State == System.ServiceModel.CommunicationState.Opened))
              _service.Close();
              _service = null;
     }

稍后编辑:哦,等一下,我刚刚重读了你的问题。您说过多次重复调用后它会失败。我猜上面的代码实际上导致了失败?

【讨论】:

  • 这段代码会成功,因为我只有几百个客户对象。将相同的模型应用于数千个发票行将失败。我开始认为我要么需要使用 ChannelFactory,要么建立一个偶尔重置服务代理的缓存机制,或者为每个服务调用重新创建代理。现在我正在进一步调查 ChannelFactory。
猜你喜欢
  • 2023-03-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多