静态 HttpClient 依然会创建 TIME_WAIT 的 TCP 端口

7
我在使用.NET Framework(4.5.1+、4.6.1和4.7.2)中的HttpClient时遇到了一些有趣的问题。因为已知存在高TCP端口使用的问题,所以我在工作项目中提出了一些更改建议,不要在每次使用时处理HttpClient。详情请见https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/
我对更改进行了调查以确保事情按预期进行,结果发现我们仍然遇到与之前相同的TIME_WAIT端口。
为了确认我的更改建议是正确的,我向应用程序添加了一些额外的跟踪,证实我在整个应用程序中都在使用同一个HttpClient实例。此后,我使用了一个简单的测试应用程序(取自上述aspnetmonsters网站)。
using System;
using System.Net.Http;

namespace ConsoleApplication
{
    public class Program
    {
        private static HttpClientHandler { UseDefaultCredentials = true };
        private static HttpClient Client = new HttpClient(handler);
        public static async Task Main(string[] args) 
        {
            Console.WriteLine("Starting connections");
            for(int i = 0; i<10; i++)
            {
                var result = await Client.GetAsync("http://localhost:51000");
                Console.WriteLine(result.StatusCode);
            }
            Console.WriteLine("Connections done");
            Console.ReadLine();
        }
    }
}

只有在使用Windows身份验证托管在IIS中的站点时,才会出现此问题。 我可以通过将身份验证设置为匿名(问题消失)然后再更改回Windows身份验证(问题重新出现)来轻松地重现该问题。
Windows身份验证的问题似乎不限于提供程序的范围。 如果您使用Negotiate或NTLM,则也存在相同的问题。 而且,如果机器只是工作站或属于域,则会出现问题。
出于兴趣,我创建了一个dotnet core 2.1.0控制台应用程序,根本没有这个问题,并且按预期工作。
简而言之:有人知道如何解决这个问题吗?还是这很可能是一个错误?

高TCP端口使用的“已知问题”不存在。该文章指出,您不需要处理HttpClient,因为它是可重用和线程安全的。它还指出,您不应该处理它,因为它可以重用SSL通道、套接字等,就像WebClient和原始HttpWebRequest一样。您使用静态HttpClient是因为您可以这样做,而不是因为有任何问题。 - Panagiotis Kanavos
“高TCP端口使用的已知问题”不存在。该文章似乎对此持不同观点。这不是一个错误,因为它是预期的行为,并且建议不要在每个请求中处理HttpClient。即使Microsoft Patterns and Practices也推荐相同的配置。只需将示例应用程序与启用Windows身份验证的IIS一起运行,您就可以看到问题。使用sysinternals的TcpView,您可以看到每个请求中留下1个处于TIME_WAIT状态的端口。 - Adam Carr
1
此外,使用静态客户端可以获得更好的性能,因为它不必每次执行DNS查找和建立SSL通道。忘记套接字吧。这是一个100倍的改进。这足以说服任何人使用共享的HttpClient实例,特别是如果你需要进行大量的HTTP调用。 - Panagiotis Kanavos
建议您在.NET Core 2.1中使用HttpClientFactory,而不仅仅是使用共享的HttpClient。它可以处理重用和清理HttpClientHandlers,这些类实际上创建连接并进行调用。定期清理是必要的,以处理DNS更新。请查看ASP.NET Core 2.1中的HttpClientFactory。该解释适用于.NET Core的一般情况。 - Panagiotis Kanavos
长话短说,如果您想重用NTLM身份验证连接,则必须使用.NET Core 2.1类和新的SocketHttpHandler。另一个选择是使用WebRequestHandler并设置UnsafeAuthenticatedConnectionSharing。有关HttpWebRequest.UnsafeAuthenticatedConnectionSharing的文档解释了风险和预防措施。 - Panagiotis Kanavos
显示剩余6条评论
1个回答

11

简短版本

如果你想要重复使用NTLM身份验证的连接,请使用.NET Core 2.1。

详细版本

我很惊讶地发现,当使用NTLM身份验证时,“旧”HttpClient为每个请求使用不同的连接。这不是一个错误 - 在.NET Core 2.1之前,HttpClient会使用HttpWebRequest,在每次进行NTLM身份验证调用后关闭连接。

这在HttpWebRequest.UnsafeAuthenticatedConnectionSharing属性的文档中有所描述,该属性可用于启用连接共享:

此属性的默认值为false,这会导致每个请求完成后关闭当前连接。您的应用程序必须在每次发出新请求时都通过认证序列。

如果将此属性设置为true,则检索响应所使用的连接在经过身份验证后保持打开状态。 在这种情况下,具有此属性设置为true的其他请求可以在不重新进行身份验证的情况下使用连接。

风险是:

如果已对用户A进行了身份验证,则用户B可能会重用A的连接; 基于用户A的凭据来满足用户B的请求。

如果理解了风险,并且应用程序不使用模拟,则可以使用WebRequestHandler配置HttpClient并设置UnsafeAuthenticatedConnectionSharing,例如:

HttpClient _client;

public void InitTheClient()
{
    var handler=new WebRequestHandler
                { 
                    UseDefaultCredentials=true,
                    UnsafeAuthenticatedConnectionSharing =true
                };
    _client=new HttpClient(handler); 
}

WebRequestHandler无法公开HttpWebRequest.ConnectionGroupName,这将允许通过ID对连接进行分组,因此它无法处理模拟。

.NET Core 2.1

HttpClient在.NET Core 2.1中被重写,并使用套接字、最小分配、连接池等实现所有的HTTP和网络功能。它还单独处理NTLM挑战/响应流程,以便可以使用相同的套接字连接来服务不同的身份验证请求。

如果有人感兴趣,可以从HttpClient追踪到SocketsHttpHanlder、HttpConnectionPoolManager、HttpConnectionPool、HttpConnection、AuthenticationHelper.NtAuth,然后返回到HttpConnection以发送原始字节。


1
谢谢你的回答,我可以看出最好的方法是迁移到.NET Core。我怀疑这是设计上的问题而不是错误,现在知道了很好。 - Adam Carr
深入浅出的解释 - Ricky
我可以确认.NET Framework和.NET Core之间提到的更改。我在GitHub上提出了一个问题以获得澄清:https://github.com/dotnet/runtime/issues/74078 在这里,我发布了一个示例,我能够重现和缓解该问题:https://github.com/rgueldenpfennig/HttpClientImpersonation.NET Framework之后的.NET版本在该领域有了很大的改进。 - Robin Güldenpfennig

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