为 ASP.NET Core 中的每个请求创建一个新的 WCF 客户端是否会导致套接字耗尽?
Can creating a new WCF client for each request in ASP.NET Core lead to socket exhaustion?
此 article 显示了一个众所周知的 HttpClient 问题,该问题可能会导致套接字耗尽。
我有一个 ASP.NET Core 3.1 网络应用程序。在 .NET Standard 2.0 class 库中,我在 Visual Studio 2019 中在此 instructions.
之后添加了 WCF Web 服务参考
在服务中,我按照 documentation 中描述的方式使用 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;
}
}
我知道不关闭客户端是不好的做法checks,但对于这个例子来说,这无关紧要。
当我启动应用程序并向使用 WCF 客户端的操作方法发出五个请求,然后查看来自 netstat 的结果时,我发现打开的连接状态为 TIME_WAIT,很像中的问题上面关于 HttpClient 的文章。
在我看来,像这样使用开箱即用的 WCF 客户端会导致套接字耗尽,还是我遗漏了什么?
WCF 客户端继承自 ClientBase<TChannel>
。阅读此 article 在我看来,WCF 客户端使用 HttpClient。如果是这样,那么我可能不应该为每个请求都创建一个新客户端,对吗?
我找到了几篇文章 (this and this) 讨论使用单例或以某种方式重用 WCF 客户端。这是要走的路吗?
###更新
调试 WCF 源代码的相应部分后,我发现每次我为每个请求创建一个新的 WCF 客户端时,都会创建一个新的 HttpClient 和 HttpClientHandler。
您可以检查代码 here
internal virtual HttpClientHandler GetHttpClientHandler(EndpointAddress to, SecurityTokenContainer clientCertificateToken)
{
return new HttpClientHandler();
}
此处理程序用于在 GetHttpClientAsync 方法中创建新的 HttpClient:
httpClient = new HttpClient(handler);
这解释了为什么在我的例子中 WCF 客户端的行为就像为每个请求创建和处理的 HttpClient。
Matt Connew 在 WCF 存储库的 issue 中写道,他使将您自己的 HttpMessage 工厂注入 WCF 客户端成为可能。
他写道:
I implemented the ability to provide a Func<HttpClientHandler,
HttpMessageHandler> to enable modifying or replacing the
HttpMessageHandler. You provide a method which takes an
HttpClientHandler and returns an HttpMessageHandler.
利用这些信息,我注入了我自己的工厂,以便能够控制 HttpClient 中 HttpClientHandlers 的生成。
我创建了自己的 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>
}
正如您在 AddBindingParameters 中看到的那样,我添加了一个非常简单的工厂,returns 一个池化的 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;
}
}
确保将对 System.ServiceModel.*
的任何包引用更新到至少版本 4.5.0 才能正常工作。如果您使用 Visual Studio 的 'Add service reference' 功能,VS 将引入这些包的 4.4.4 版本(使用 Visual Studio 16.8.4 测试)。
当我 运行 具有这些更改的应用程序时,我不再为我发出的每个请求打开连接。
您应该考虑处置您的 CalculatorSoapClient
。请注意,由于 ClientBase 的实现,简单的 Dispose()
通常是不够的。
看看https://docs.microsoft.com/en-us/dotnet/framework/wcf/samples/use-close-abort-release-wcf-client-resources?redirectedfrom=MSDN,那里解释了问题。
还要考虑到底层代码正在管理您的连接,有时它会让它们保持活动状态以备后用。尝试多次调用服务器以查看每次调用是否有新连接,或者连接是否被重用。
TIME_WAIT的意思也在这里讨论:
https://superuser.com/questions/173535/what-are-close-wait-and-time-wait-states
看起来您的客户端已经完成了关闭连接所需的一切,只是在等待服务器的确认。
您不必使用单例,因为框架(通常)会很好地处理连接。
我在 Github 的 WCF 存储库中创建了一个 issue 并得到了一些很好的答案。
根据该领域权威人士 Matt Connew 和 Stephen Bonikowsky 的说法,最好的解决方案是重用客户端或 ChannelFactory。
Bonikowsky 写道:
Create a single client and re-use it.
var client = new ImportSoapClient();
Connew 补充道:
Another possibility is you could create a channel proxy instance from
the underlying channelfactory. You would do this with code similar to
this:
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();
}
根据 Connew 的说法,在我的 ASP.NET 具有潜在并发请求的核心 Web 应用程序中重用客户端没有问题。
Concurrent requests all using the same client is not a problem as long
as you explicitly open the channel before any requests are made. If
using a channel created from the channel factory, you can do this with
((IClientChannel)proxy).Open();. I believe the generated client also
adds an OpenAsync method that you can use.
更新
由于重用 WCF 客户端也意味着重用 HttpClient
实例,这可能导致已知的 DNS problem 我决定使用我自己的 IEndpointBehavior
实现来使用我的原始解决方案如问题中所述。
此 article 显示了一个众所周知的 HttpClient 问题,该问题可能会导致套接字耗尽。
我有一个 ASP.NET Core 3.1 网络应用程序。在 .NET Standard 2.0 class 库中,我在 Visual Studio 2019 中在此 instructions.
之后添加了 WCF Web 服务参考在服务中,我按照 documentation 中描述的方式使用 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;
}
}
我知道不关闭客户端是不好的做法checks,但对于这个例子来说,这无关紧要。
当我启动应用程序并向使用 WCF 客户端的操作方法发出五个请求,然后查看来自 netstat 的结果时,我发现打开的连接状态为 TIME_WAIT,很像中的问题上面关于 HttpClient 的文章。
在我看来,像这样使用开箱即用的 WCF 客户端会导致套接字耗尽,还是我遗漏了什么?
WCF 客户端继承自 ClientBase<TChannel>
。阅读此 article 在我看来,WCF 客户端使用 HttpClient。如果是这样,那么我可能不应该为每个请求都创建一个新客户端,对吗?
我找到了几篇文章 (this and this) 讨论使用单例或以某种方式重用 WCF 客户端。这是要走的路吗?
###更新
调试 WCF 源代码的相应部分后,我发现每次我为每个请求创建一个新的 WCF 客户端时,都会创建一个新的 HttpClient 和 HttpClientHandler。 您可以检查代码 here
internal virtual HttpClientHandler GetHttpClientHandler(EndpointAddress to, SecurityTokenContainer clientCertificateToken)
{
return new HttpClientHandler();
}
此处理程序用于在 GetHttpClientAsync 方法中创建新的 HttpClient:
httpClient = new HttpClient(handler);
这解释了为什么在我的例子中 WCF 客户端的行为就像为每个请求创建和处理的 HttpClient。
Matt Connew 在 WCF 存储库的 issue 中写道,他使将您自己的 HttpMessage 工厂注入 WCF 客户端成为可能。 他写道:
I implemented the ability to provide a Func<HttpClientHandler, HttpMessageHandler> to enable modifying or replacing the HttpMessageHandler. You provide a method which takes an HttpClientHandler and returns an HttpMessageHandler.
利用这些信息,我注入了我自己的工厂,以便能够控制 HttpClient 中 HttpClientHandlers 的生成。
我创建了自己的 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>
}
正如您在 AddBindingParameters 中看到的那样,我添加了一个非常简单的工厂,returns 一个池化的 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;
}
}
确保将对 System.ServiceModel.*
的任何包引用更新到至少版本 4.5.0 才能正常工作。如果您使用 Visual Studio 的 'Add service reference' 功能,VS 将引入这些包的 4.4.4 版本(使用 Visual Studio 16.8.4 测试)。
当我 运行 具有这些更改的应用程序时,我不再为我发出的每个请求打开连接。
您应该考虑处置您的 CalculatorSoapClient
。请注意,由于 ClientBase 的实现,简单的 Dispose()
通常是不够的。
看看https://docs.microsoft.com/en-us/dotnet/framework/wcf/samples/use-close-abort-release-wcf-client-resources?redirectedfrom=MSDN,那里解释了问题。
还要考虑到底层代码正在管理您的连接,有时它会让它们保持活动状态以备后用。尝试多次调用服务器以查看每次调用是否有新连接,或者连接是否被重用。
TIME_WAIT的意思也在这里讨论:
https://superuser.com/questions/173535/what-are-close-wait-and-time-wait-states
看起来您的客户端已经完成了关闭连接所需的一切,只是在等待服务器的确认。
您不必使用单例,因为框架(通常)会很好地处理连接。
我在 Github 的 WCF 存储库中创建了一个 issue 并得到了一些很好的答案。
根据该领域权威人士 Matt Connew 和 Stephen Bonikowsky 的说法,最好的解决方案是重用客户端或 ChannelFactory。
Bonikowsky 写道:
Create a single client and re-use it.
var client = new ImportSoapClient();
Connew 补充道:
Another possibility is you could create a channel proxy instance from the underlying channelfactory. You would do this with code similar to this:
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();
}
根据 Connew 的说法,在我的 ASP.NET 具有潜在并发请求的核心 Web 应用程序中重用客户端没有问题。
Concurrent requests all using the same client is not a problem as long as you explicitly open the channel before any requests are made. If using a channel created from the channel factory, you can do this with ((IClientChannel)proxy).Open();. I believe the generated client also adds an OpenAsync method that you can use.
更新
由于重用 WCF 客户端也意味着重用 HttpClient
实例,这可能导致已知的 DNS problem 我决定使用我自己的 IEndpointBehavior
实现来使用我的原始解决方案如问题中所述。