Статический HttpClient все еще создает TCP-порты TIME_WAIT

У меня интересное поведение с HttpClient из .NET Framework (4.5.1+, 4.6.1 и 4.7.2). Я предложил некоторые изменения в рабочем проекте, чтобы не удалять HttpClient при каждом использовании из-за известной проблемы с интенсивным использованием порта TCP, см. 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();
        }
    }
}

Проблема возникает только при подключении к сайту, размещенному в IIS, с использованием проверки подлинности Windows. Я могу легко воспроизвести проблему, установив для параметра Аутентификация значение Анонимный (проблема исчезнет) и обратно на Аутентификацию Windows (проблема повторяется).

Проблема с проверкой подлинности Windows, по-видимому, не ограничивается рамками поставщика. У него та же проблема, если вы используете Negotiate или NTLM. Также проблема возникает, если машина является просто рабочей станцией или частью домена.

Из интереса я создал консольное приложение dotnet core 2.1.0, и проблема вообще отсутствует и работает как положено.

TLDR: Кто-нибудь знает, как это исправить, или это, скорее всего, ошибка?


person Adam Carr    schedule 08.07.2018    source источник
comment
Нет известных проблем с высоким использованием TCP-порта. В этой статье говорится, что вам не нужно избавляться от HttpClient, потому что он может использоваться повторно и является потокобезопасным. В нем также говорится, что вы не должны удалять его, потому что он может повторно использовать SSL-каналы, сокеты и т. д., как это делают WebClient и необработанный HttpWebRequest. Вы используете статический HttpClient, потому что можете, а не из-за каких-либо проблем   -  person Panagiotis Kanavos    schedule 10.07.2018
comment
«Нет известных проблем с интенсивным использованием TCP-порта». Эта статья, кажется, аргументирует точку зрения по-другому. Это не ошибка, поскольку это предполагаемое поведение, и рекомендуется не удалять HttpClient при каждом запросе. Даже Microsoft Patterns and Practices также рекомендует ту же конфигурацию. Просто запустив пример приложения для IIS с проверкой подлинности Windows, вы увидите проблему. Используйте TcpView от sysinternals, и вы увидите, что 1 порт остается в TIME_WAIT на запрос.   -  person Adam Carr    schedule 10.07.2018
comment
Нет, это не так. Это не говорит о проблеме с HttpClient - это даже не подразумевается. В нем говорится, что вы можете повторно использовать экземпляр HttpClient и сохранить некоторые сокеты. Это тоже не новость, это известно с 2012 года. Хотя эта статья, пожалуй, самая популярная в наши дни.   -  person Panagiotis Kanavos    schedule 10.07.2018
comment
Кстати, нет репродукции. Выполнение кода статьи ведет себя так, как ожидалось. Использование TcpView показывает, что использование статического HttpClient не оставляет никаких сокетов. Запуск его с новым HttpClient каждый раз оставляет открытыми 10 сокетов. Мне пришлось дождаться закрытия этих сокетов, прежде чем запускать тест со статическим клиентом.   -  person Panagiotis Kanavos    schedule 10.07.2018
comment
Кроме того, использование статического клиента привело к гораздо большей производительности, поскольку ему не нужно было каждый раз выполнять поиск в DNS и устанавливать канал SSL. Забудьте о розетках. Это было 100-кратное улучшение. Этого должно быть достаточно, чтобы убедить любого использовать общий экземпляр HttpClient, особенно если вы делаете много HTTP-вызовов.   -  person Panagiotis Kanavos    schedule 10.07.2018
comment
Вместо того, чтобы просто использовать общий HttpClient, я бы посоветовал вам использовать HttpClientFactory, особенно в .NET Core 2.1. Он заботится о повторном использовании и очистке HttpClientHandlers, классов, которые фактически создают соединения и выполняют вызовы. Периодическая очистка необходима для обновления DNS. Проверьте HttpClientFactory в ASP.NET Core 2.1. Объяснение относится к .NET Core в целом.   -  person Panagiotis Kanavos    schedule 10.07.2018
comment
Вам также следует ознакомиться с серией статей Скотта Ханслемана на HttpClientFactory. Например, Добавление устойчивости и обработки временных сбоев в ваш .NET Core HttpClient с помощью Polly показывает, как вы может комбинировать HttpClientFactory и Polly для создания экземпляров HttpClient, которые, например, автоматически повторяют попытку в случае ошибки и прекращают попытки через некоторое время   -  person Panagiotis Kanavos    schedule 10.07.2018
comment
@PanagiotisKanavos, когда вы тестировали, использовала ли конечная точка, к которой вы подключаетесь, проверку подлинности Windows? и HttpClient передает учетные данные через HttpClientHandler? При использовании без аутентификации на конечной точке у меня тоже все работает. Также это влияет только на .NET Framework, а не на ядро ​​dotnet.   -  person Adam Carr    schedule 10.07.2018
comment
Я вызвал службу с проверкой подлинности Windows и воспроизвел то, что вы описываете. Однако это не ошибка в HttpClient. NTLM необходимо аутентифицировать каждое соединение, а это означает, что по умолчанию каждый запрос создает новое соединение, если только UnsafeAuthenticatedConnectionSharing установлен. В полной структуре HttpClient использует HttpWebRequest, который использует WinHTTP и такое поведение.   -  person Panagiotis Kanavos    schedule 10.07.2018
comment
В .NET Core 2.1 HttpClient полностью переписан и позволяет избежать создания новых соединений путем объединения соединений и обработка самого потока аутентификации NT. Один и тот же код теперь работает в Windows и Linux, а до Core 2.0 он вызывал WinHttp в Windows и libcurl в Windows. Вы можете преследовать вызовы от HttpClient к SocketsHttpHanlder, к HttpConnectionPoolManager, HttpConnectionPool, HttpConnection к AuthenticationHelper.NtAuth.   -  person Panagiotis Kanavos    schedule 10.07.2018
comment
Короче говоря, вам придется использовать класс .NET Core 2.1 и новый SocketHttpHandler, если вы хотите повторно использовать подключения с проверкой подлинности NTLM. Другой вариант — использовать WebRequestHandler и установить UnsafeAuthenticatedConnectionSharing. Документация для HttpWebRequest. UnsafeAuthenticatedConnectionSharing объясняет риски и меры предосторожности   -  person Panagiotis Kanavos    schedule 10.07.2018


Ответы (1)


Краткая версия

Используйте .NET Core 2.1, если вы хотите повторно использовать соединения с проверкой подлинности NTLM.

Длинная версия

Я был очень удивлен, увидев, что старый HttpClient использует другое соединение для каждого запроса, когда используется аутентификация NTLM. Это не ошибка — до того, как .NET Core 2.1 HttpClient использовал HttpWebRequest, который закрывает соединение после каждого вызова с проверкой подлинности NTLM.

Это описано в документации по HttpWebRequest.UnsafeAuthenticatedConnectionSharing, которое можно использовать для включения общего доступа к соединению:

Значение по умолчанию для этого свойства — false, что приводит к закрытию текущего соединения после завершения запроса. Ваше приложение должно проходить последовательность проверки подлинности каждый раз, когда оно отправляет новый запрос.

Если для этого свойства задано значение true, соединение, используемое для получения ответа, остается открытым после выполнения проверки подлинности. В этом случае другие запросы, у которых для этого свойства задано значение true, могут использовать соединение без повторной проверки подлинности.

Риск в том, что:

Если соединение было аутентифицировано для пользователя A, пользователь B может повторно использовать соединение A; запрос пользователя B выполняется на основе учетных данных пользователя A.

Если кто-то понимает риски и приложение не использует олицетворение, можно настроить HttpClient с WebRequestHandler и установите UnsafeAuthenticatedConnectionSharing, например:

HttpClient _client;

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

WebRequestHandler не предоставляет HttpWebRequest.ConnectionGroupName, который позволит группировать соединения, например, по идентификатору, чтобы он не мог обрабатывать олицетворение.

.NET Core 2.1

HttpClient был переписан в .NET Core 2.1 и реализует все HTTP, сетевые функции с использованием сокетов, минимальные выделения, пул соединений и т. д. Он также обрабатывает поток запроса/ответа NTLM отдельно, чтобы можно было использовать одно и то же подключение к сокету для обслуживания различных аутентифицированных запросов.

Если кому-то интересно, вы можете преследовать вызовы от HttpClient к SocketsHttpHanlder, к HttpConnectionPoolManager, HttpConnectionPool, HttpConnection, AuthenticationHelper.NtAuth, а затем обратно к HttpConnection для отправки необработанных байтов.

person Panagiotis Kanavos    schedule 10.07.2018
comment
Спасибо за ответ, я вижу, что лучше всего перейти на .NET Core. У меня было подозрение, что это было задумано, а не ошибкой, хотя это полезно знать. - person Adam Carr; 10.07.2018
comment
Проницательное объяснение - person Ricky; 27.02.2019