为 ASP.NET Core 中的每个请求创建新的 WCF 客户端是否会导致套接字耗尽?

Edm*_*son 5 .net asp.net wcf web-services

本文介绍了 HttpClient 的一个众所周知的问题,该问题可能导致套接字耗尽

我有一个 ASP.NET Core 3.1 Web 应用程序。在 .NET Standard 2.0 类库中,我按照此说明在 Visual Studio 2019 中添加了 WCF Web 服务引用。

在服务中,我按照文档中描述的方式使用 WCF 客户端。创建 WCF 客户端的实例,然后针对每个请求关闭客户端。

public class TestService
{
    public async Task<int> Add(int a, int b)
    {
        CalculatorSoapClient client = new CalculatorSoapClient();
        var resultat = await client.AddAsync(a, b);
        //this is a bad way to close the client I should also check
        //if I need to call Abort()
        await client.CloseAsync();
        return resultat;
    }
}
Run Code Online (Sandbox Code Playgroud)

我知道在没有任何检查的情况下关闭客户端是不好的做法,但就本示例而言,这并不重要。

当我启动应用程序并向使用 WCF 客户端的操作方法发出五个请求,然后查看 netstat 的结果时,我发现状态为 TIME_WAIT 的打开连接,与上面有关 HttpClient 的文章中的问题非常相似。

在此输入图像描述

在我看来,像这样使用开箱即用的 WCF 客户端可能会导致套接字耗尽,或者我是否遗漏了某些内容?

WCF 客户端继承自ClientBase<TChannel>. 阅读这篇文章,我觉得 WCF 客户端使用 HttpClient。如果是这种情况,那么我可能不应该为每个请求创建一个新客户端,对吧?

我发现了几篇文章(这篇这篇)讨论使用单例或以某种方式重用 WCF 客户端。这是要走的路吗?

###更新

调试 WCF 源代码的相应部分,我发现每次我为每个请求创建新的 WCF 客户端时,都会创建一个新的 HttpClient 和 HttpClientHandler。您可以在此处检查代码

internal virtual HttpClientHandler GetHttpClientHandler(EndpointAddress to, SecurityTokenContainer clientCertificateToken)
{
    return new HttpClientHandler();
}
Run Code Online (Sandbox Code Playgroud)

此处理程序用于在 GetHttpClientAsync 方法中创建新的 HttpClient:

httpClient = new HttpClient(handler);
Run Code Online (Sandbox Code Playgroud)

这解释了为什么在我的例子中,WCF 客户端的行为就像为每个请求创建和处置的 HttpClient。

Matt Connew 在 WCF 存储库的一个问题中写道,他已经可以将您自己的 HttpMessage 工厂注入到 WCF 客户端中。他写:

我实现了提供 Func<HttpClientHandler, HttpMessageHandler> 的功能,以允许修改或替换 HttpMessageHandler。您提供一个接受 HttpClientHandler 并返回 HttpMessageHandler 的方法。

使用这些信息,我注入了自己的工厂,以便能够控制 HttpClient 中 HttpClientHandler 的生成。

我创建了自己的 IEndpointBehavior 实现,它注入 IHttpMessageHandlerFactory 以获取池化的 HttpMessageHandler。

public class MyEndpoint : IEndpointBehavior
{
    private readonly IHttpMessageHandlerFactory messageHandlerFactory;

    public MyEndpoint(IHttpMessageHandlerFactory messageHandlerFactory)
    {
        this.messageHandlerFactory = messageHandlerFactory;
    }

    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {
        Func<HttpClientHandler, HttpMessageHandler> myHandlerFactory = (HttpClientHandler clientHandler) =>
        {
            return messageHandlerFactory.CreateHandler();
        };
        bindingParameters.Add(myHandlerFactory);
    }

    <other empty methods needed for implementation of IEndpointBehavior>

}
Run Code Online (Sandbox Code Playgroud)

正如您在 AddBindingParameters 中看到的,我添加了一个非常简单的工厂,它返回池化的 HttpMessageHandler。

我将此行为添加到我的 WCF 客户端,如下所示。

public class TestService
{
    private readonly MyEndpoint endpoint;

    public TestService(MyEndpoint endpoint)
    {
        this.endpoint = endpoint;
    }

    public async Task<int> Add(int a, int b)
    {
        CalculatorSoapClient client = new CalculatorSoapClient();
        client.Endpoint.EndpointBehaviors.Add(endpoint);
        var resultat = await client.AddAsync(a, b);
        //this is a bad way to close the client I should also check
        //if I need to call Abort()
        await client.CloseAsync();
        return resultat;
    }
}
Run Code Online (Sandbox Code Playgroud)

请务必将所有包引用System.ServiceModel.*至少更新到版本4.5.0才能正常工作。如果您使用 Visual Studio 的“添加服务引用”功能,VS 将提取这些包的 4.4.4 版本(使用 Visual Studio 16.8.4 进行测试)。

当我运行具有这些更改的应用程序时,我不再为我发出的每个请求建立开放连接。

Edm*_*son 1

我创建了一个在 Github 的 WCF 存储库中创建了一个问题,并得到了一些很好的答案。

根据该领域权威 Matt Connew 和 Stephen Bonikowsky 的说法,最好的解决方案是重用客户端或 ChannelFactory。

博尼科夫斯基写道:

创建一个客户端并重复使用它。

var client = new ImportSoapClient();
Run Code Online (Sandbox Code Playgroud)

康纽补充道:

另一种可能性是您可以从底层通道工厂创建通道代理实例。您可以使用类似于以下的代码来执行此操作:

public void Init()
{
    _client?.Close();
    _factory?.Close();
    _client = new ImportSoapClient();
    _factory = client.ChannelFactory;
}

public void DoWork()
{
    var proxy = _factory.CreateChannel();
    proxy.MyOperation();
    ((IClientChannel)proxy).Close();
}
Run Code Online (Sandbox Code Playgroud)

根据 Connew 的说法,在具有潜在并发请求的 ASP.NET Core Web 应用程序中重用客户端没有问题。

只要您在发出任何请求之前显式打开通道,所有使用同一客户端的并发请求就不是问题。如果使用从通道工厂创建的通道,则可以使用 ((IClientChannel)proxy).Open(); 来执行此操作。我相信生成的客户端还添加了一个您可以使用的 OpenAsync 方法。

更新

由于重用 WCF 客户端也意味着重用实例HttpClient,这可能会导致已知的DNS 问题,因此我决定使用我自己的实现(IEndpointBehavior如问题中所述)采用原始解决方案。