Должны ли мы создать новый единственный экземпляр HttpClient для всех запросов?

58

недавно я наткнулся на это сообщение в блоге от asp.net monsters, в котором говорится о проблемах с использованием HttpClientследующим образом:

using(var client = new HttpClient())
{
}

Согласно сообщению в блоге, если мы располагаем HttpClientпосле каждого запроса, он может держать соединения TCP открытыми. Это может потенциально привести к System.Net.Sockets.SocketException.

Правильный способ в соответствии с постом состоит в том, чтобы создать один экземпляр, так HttpClientкак это помогает уменьшить растрату сокетов.

Из поста:

Если мы будем использовать один экземпляр HttpClient, мы сможем сократить потери сокетов, используя их повторно:

namespace ConsoleApplication
{
    public class Program
    {
        private static HttpClient Client = new HttpClient();
        public static void Main(string[] args)
        {
            Console.WriteLine("Starting connections");
            for(int i = 0; i<10; i++)
            {
                var result = Client.GetAsync("http://aspnetmonsters.com").Result;
                Console.WriteLine(result.StatusCode);
            }
            Console.WriteLine("Connections done");
            Console.ReadLine();
        }
    }
}

Я всегда выбрасывал HttpClientобъект после его использования, так как чувствовал, что это лучший способ его использования. Но этот пост в блоге заставляет меня чувствовать, что я все это время делал неправильно.

Должны ли мы создать новый единый экземпляр HttpClientдля всех запросов? Есть ли подводные камни в использовании статического экземпляра?

Анкит Виджай
источник
Сталкивались ли вы с какими-либо проблемами, связанными с тем, как вы их используете?
whatsisname
Может быть, проверить этот ответ, а также это .
Джон Ву
@whatsisname нет у меня нет, но, просматривая блог, я почувствовал, что могу все время использовать это неправильно. Следовательно, хотел понять от других разработчиков, видят ли они какую-либо проблему в любом подходе.
Анкит Виджай
3
Я сам не пробовал (поэтому не предоставил это в качестве ответа), но в соответствии с Microsoft от .NET Core 2.1 вы должны использовать HttpClientFactory, как описано на docs.microsoft.com/en-us/dotnet/standard/ …
Джори Себрехтс
(Как указано в моем ответе, я просто хотел сделать его более заметным, поэтому я пишу короткий комментарий.) Статический экземпляр будет правильно обрабатывать рукопожатие при закрытии соединения tcp, как только вы сделаете Close()или инициируете новое Get(). Если вы просто избавитесь от клиента, когда закончите с ним, никто не сможет справиться с этим заключительным рукопожатием, и все ваши порты будут в состоянии TIME_WAIT, из-за этого.
Младен Б.

Ответы:

40

Это похоже на убедительный пост в блоге. Однако, прежде чем принять решение, я сначала запустил те же тесты, что и автор блога, но на вашем собственном коде. Я также попытался бы узнать немного больше о HttpClient и его поведении.

Этот пост гласит:

Экземпляр HttpClient представляет собой набор настроек, применяемых ко всем запросам, выполняемым этим экземпляром. Кроме того, каждый экземпляр HttpClient использует собственный пул соединений, изолируя свои запросы от запросов, выполняемых другими экземплярами HttpClient.

Итак, что, вероятно, происходит при совместном использовании HttpClient, это то, что соединения используются повторно, что хорошо, если вам не требуются постоянные соединения. Единственный способ узнать наверняка, имеет ли это значение для вашей ситуации, - это запустить собственные тесты производительности.

Если вы покопаетесь, вы найдете несколько других ресурсов, посвященных этой проблеме (включая статью Microsoft Best Practices), так что, вероятно, это хорошая идея для реализации в любом случае (с некоторыми мерами предосторожности).

Рекомендации

Вы используете Httpclient неправильно, и это дестабилизирует ваше программное обеспечение
Singleton HttpClient? Остерегайтесь этого серьезного поведения и способов его исправления.
Microsoft Patterns and Practices - Оптимизация производительности: неправильная реализация
Один экземпляр многоразового использования HttpClient при проверке кода
Singleton HttpClient не учитывает изменения DNS (CoreFX)
Общие рекомендации по использованию HttpClient

Роберт Харви
источник
1
Это хороший обширный список. Это мои выходные прочитанные.
Анкит Виджай
«Если вы покопаетесь, вы найдете несколько других ресурсов, посвященных этой проблеме ...» Вы хотите сказать, что проблема с TCP-соединением открыта?
Анкит Виджай
Краткий ответ: используйте статический HttpClient . Если вам нужно поддерживать изменения DNS (вашего веб-сервера или других серверов), вам нужно беспокоиться о настройках тайм-аута.
Джесс
3
Это свидетельство того, насколько запутанным HttpClient является то, что его использование - «чтение в выходные дни», как прокомментировал @AnkitVijay.
usr
@ Джесс, кроме изменений в DNS - пропуск трафика всего вашего клиента через один сокет также нарушит балансировку нагрузки?
Иан
16

Я опаздываю на вечеринку, но вот мое учебное путешествие по этой хитрой теме.

1. Где мы можем найти официального адвоката по повторному использованию HttpClient?

Я имею в виду, что если повторное использование HttpClient предназначено и это важно , то такой адвокат лучше задокументирован в собственной документации API, а не скрыт во множестве «Расширенных тем», «Производительных (анти) шаблонов» или других постов в блоге. , Иначе как новый ученик должен знать это, пока не стало слишком поздно?

На данный момент (май 2018 г.) первый результат поиска при поиске в Google «c # httpclient» указывает на эту справочную страницу API в MSDN , которая вообще не упоминает об этом намерении. Итак, урок 1 для новичка: всегда нажимайте ссылку «Другие версии» сразу после заголовка страницы справки MSDN, там вы, вероятно, найдете ссылки на «текущую версию». В этом случае HttpClient, он приведет вас к последнему документу, содержащему описание этого намерения .

Я подозреваю, что многие разработчики, которые были новичками в этой теме, также не нашли правильную страницу документации, поэтому эти знания не получили широкого распространения, и люди были удивлены, когда узнали об этом позже , возможно, с трудом .

2. Понятие (не так?) using IDisposable

Это одна немного не по теме , но все же стоит отметить, что это не совпадение , чтобы увидеть людей в тех вышеупомянутых блогах обвиняющих как HttpClient«S IDisposableинтерфейс делает их , как правило , использовать using (var client = new HttpClient()) {...}шаблон , а затем привести к этой проблеме.

Я полагаю, что это сводится к негласной (ошибочной?) Концепции: «ожидается, что IDisposable объект будет недолговечным» .

ОДНАКО, хотя это, конечно, выглядит недолгим, когда мы пишем код в этом стиле:

using (var foo = new SomeDisposableObject())
{
    ...
}

официальная документация по IDisposable никогда не упоминает IDisposableобъекты должны быть кратковременными. По определению, IDisposable - это просто механизм, позволяющий вам высвобождать неуправляемые ресурсы. Ничего больше. В этом смысле вы ОЖИДАЕТЕ в конечном итоге инициировать утилизацию, но это не требует, чтобы вы делали это недолгим образом.

Поэтому ваша задача - правильно выбрать, когда инициировать утилизацию, исходя из требований жизненного цикла вашего реального объекта. Ничто не мешает вам использовать IDisposable в течение длительного времени:

using System;
namespace HelloWorld
{
    class Hello
    {
        static void Main()
        {
            Console.WriteLine("Hello World!");

            using (var client = new HttpClient())
            {
                for (...) { ... }  // A really long loop

                // Or you may even somehow start a daemon here

            }

            // Keep the console window open in debug mode.
            Console.WriteLine("Press any key to exit.");
            Console.ReadKey();
        }
    }
}

С этим новым пониманием, теперь, когда мы возвращаемся к этому сообщению в блоге , мы можем четко заметить, что «исправление» инициализируется HttpClientодин раз, но никогда не удаляет его, поэтому мы можем видеть из его вывода netstat, что соединение остается в состоянии ESTABLISHED, что означает, что оно имеет НЕ был правильно закрыт. Если бы он был закрыт, его состояние было бы в TIME_WAIT. На практике нет ничего страшного в том, чтобы утратить только одно открытое соединение после завершения всей вашей программы, и постер блога все еще видит увеличение производительности после исправления; но все же, концептуально неправильно обвинять IDisposable и выбирать НЕ распоряжаться им.

3. Нужно ли нам помещать HttpClient в статическое свойство или даже в качестве одиночного?

Исходя из понимания предыдущего раздела, я думаю, что ответ здесь становится ясным: «не обязательно». Это действительно зависит от того, как вы организуете свой код, если вы повторно используете HttpClient И (в идеале) утилизируете его в конце концов.

Весело, что даже пример в разделе « Замечания» текущего официального документа не делает это строго правильно. Он определяет класс «GoodController», содержащий статическое свойство HttpClient, которое не будет утилизироваться; что не подчиняется тому, что подчеркивает другой пример в разделе «Примеры» : «необходимо вызвать dispose ... чтобы приложение не теряло ресурсы».

И, наконец, синглтон не без собственных проблем.

«Сколько людей считают глобальную переменную хорошей идеей? Никто.

Сколько людей считают синглтон хорошей идеей? Несколько.

Что дает? Синглтоны - это просто набор глобальных переменных ".

- Цитируется из этой вдохновляющей лекции "Глобальное государство и синглтоны"

PS: SqlConnection

Этот вопрос не имеет отношения к текущим вопросам и ответам, но, вероятно, его следует знать. Схема использования SqlConnection отличается. Вам НЕ нужно повторно использовать SqlConnection , потому что он будет лучше обрабатывать свой пул соединений.

Разница обусловлена ​​их подходом к реализации. Каждый экземпляр HttpClient использует свой собственный пул соединений (цитируется здесь ); но сам SqlConnection управляется центральным пулом соединений, в соответствии с этим .

И вам все еще нужно избавиться от SqlConnection, так же, как вы должны делать для HttpClient.

RayLuo
источник
14

Я сделал некоторые тесты увидеть улучшения производительности со статическим HttpClient. Я использовал приведенный ниже код для моего тестирования:

namespace HttpClientTest
{
    using System;
    using System.Net.Http;

    class Program
    {
        private static readonly int _connections = 10;
        private static readonly HttpClient _httpClient = new HttpClient();

        private static void Main()
        {
            TestHttpClientWithStaticInstance();
            TestHttpClientWithUsing();
        }

        private static void TestHttpClientWithUsing()
        {
            try
            {
                for (var i = 0; i < _connections; i++)
                {
                    using (var httpClient = new HttpClient())
                    {
                        var result = httpClient.GetAsync(new Uri("http://bing.com")).Result;
                    }
                }
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception);
            }
        }

        private static void TestHttpClientWithStaticInstance()
        {
            try
            {
                for (var i = 0; i < _connections; i++)
                {
                    var result = _httpClient.GetAsync(new Uri("http://bing.com")).Result;
                }
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception);
            }
        }
    }
}

Для тестирования:

  • Я запустил код с 10, 100, 1000 и 1000 подключений.
  • Провел каждый тест 3 раза, чтобы узнать среднее значение.
  • Выполняется по одному методу за раз

Я обнаружил улучшение производительности от 40% до 60% при использовании статического HttpClientвместо того, чтобы утилизировать его для HttpClientзапроса. Я поместил детали результата теста производительности в сообщение в блоге здесь .

Анкит Виджай
источник
1

Чтобы правильно закрыть соединение TCP , нам нужно завершить последовательность пакетов FIN - FIN + ACK - ACK (точно так же, как SYN - SYN + ACK - ACK, при открытии соединения TCP ). Если мы просто вызываем метод .Close () (обычно это происходит при удалении HttpClient ), и мы не ждем, пока удаленная сторона подтвердит наш запрос на закрытие (с помощью FIN + ACK), мы получим состояние TIME_WAIT на локальный порт TCP, потому что мы избавились от нашего слушателя (HttpClient), и у нас никогда не было возможности сбросить состояние порта в надлежащее закрытое состояние, как только удаленный узел отправит нам пакет FIN + ACK.

Правильный способ закрыть TCP-соединение - вызвать метод .Close () и подождать, пока событие close с другой стороны (FIN + ACK) придет на нашу сторону. Только тогда мы можем отправить наш окончательный ACK и утилизировать HttpClient.

Просто добавьте, что имеет смысл держать TCP-соединения открытыми, если вы выполняете HTTP-запросы, из-за HTTP-заголовка «Connection: Keep-Alive». Более того, вы можете попросить удаленный узел закрыть соединение вместо вас, установив HTTP-заголовок «Connection: Close». Таким образом, ваши локальные порты всегда будут правильно закрыты, вместо того, чтобы находиться в состоянии TIME_WAIT.

Младен Б.
источник
1

Вот базовый клиент API, который эффективно использует HttpClient и HttpClientHandler. Когда вы создаете новый HttpClient для выполнения запроса, возникает много накладных расходов. НЕ воссоздайте HttpClient для каждого запроса. Повторно используйте HttpClient ...

using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
//You need to install package Newtonsoft.Json > https://www.nuget.org/packages/Newtonsoft.Json/
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;


public class MyApiClient : IDisposable
{
    private readonly TimeSpan _timeout;
    private HttpClient _httpClient;
    private HttpClientHandler _httpClientHandler;
    private readonly string _baseUrl;
    private const string ClientUserAgent = "my-api-client-v1";
    private const string MediaTypeJson = "application/json";

    public MyApiClient(string baseUrl, TimeSpan? timeout = null)
    {
        _baseUrl = NormalizeBaseUrl(baseUrl);
        _timeout = timeout ?? TimeSpan.FromSeconds(90);    
    }

    public async Task<string> PostAsync(string url, object input)
    {
        EnsureHttpClientCreated();

        using (var requestContent = new StringContent(ConvertToJsonString(input), Encoding.UTF8, MediaTypeJson))
        {
            using (var response = await _httpClient.PostAsync(url, requestContent))
            {
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsStringAsync();
            }
        }
    }

    public async Task<TResult> PostAsync<TResult>(string url, object input) where TResult : class, new()
    {
        var strResponse = await PostAsync(url, input);

        return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    public async Task<TResult> GetAsync<TResult>(string url) where TResult : class, new()
    {
        var strResponse = await GetAsync(url);

        return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    public async Task<string> GetAsync(string url)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.GetAsync(url))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public async Task<string> PutAsync(string url, object input)
    {
        return await PutAsync(url, new StringContent(JsonConvert.SerializeObject(input), Encoding.UTF8, MediaTypeJson));
    }

    public async Task<string> PutAsync(string url, HttpContent content)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.PutAsync(url, content))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public async Task<string> DeleteAsync(string url)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.DeleteAsync(url))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public void Dispose()
    {
        _httpClientHandler?.Dispose();
        _httpClient?.Dispose();
    }

    private void CreateHttpClient()
    {
        _httpClientHandler = new HttpClientHandler
        {
            AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip
        };

        _httpClient = new HttpClient(_httpClientHandler, false)
        {
            Timeout = _timeout
        };

        _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(ClientUserAgent);

        if (!string.IsNullOrWhiteSpace(_baseUrl))
        {
            _httpClient.BaseAddress = new Uri(_baseUrl);
        }

        _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeJson));
    }

    private void EnsureHttpClientCreated()
    {
        if (_httpClient == null)
        {
            CreateHttpClient();
        }
    }

    private static string ConvertToJsonString(object obj)
    {
        if (obj == null)
        {
            return string.Empty;
        }

        return JsonConvert.SerializeObject(obj, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    private static string NormalizeBaseUrl(string url)
    {
        return url.EndsWith("/") ? url : url + "/";
    }
}

Использование:

using (var client = new MyApiClient("http://localhost:8080"))
{
    var response = client.GetAsync("api/users/findByUsername?username=alper").Result;
    var userResponse = client.GetAsync<MyUser>("api/users/findByUsername?username=alper").Result;
}
Альпер Эбикоглу
источник
-5

Нет единственного способа использовать класс HttpClient. Ключ заключается в том, чтобы спроектировать ваше приложение так, чтобы оно имело смысл для его среды и ограничений.

HTTP - это отличный протокол, который нужно использовать, когда вам нужно предоставить общедоступные API. Он также может эффективно использоваться для облегченных внутренних служб с низкой задержкой - хотя шаблон очереди сообщений RPC часто является лучшим выбором для внутренних служб.

Существует много сложностей в правильном выполнении HTTP.

Учтите следующее:

  1. Создание сокета и установление TCP-соединения использует пропускную способность сети и время.
  2. HTTP / 1.1 поддерживает конвейерные запросы на одном сокете. Отправка нескольких запросов один за другим, без необходимости ждать предыдущих ответов - это, вероятно, ответственно за улучшение скорости, о котором сообщается в блоге.
  3. Кэширование и балансировка нагрузки - если у вас есть балансировщик нагрузки перед серверами, то обеспечение наличия у ваших запросов соответствующих заголовков кэша может снизить нагрузку на ваши серверы и быстрее получить ответы на запросы клиентов.
  4. Никогда не опрашивайте ресурс, используйте HTTP-чанкинг для получения периодических ответов.

Но, прежде всего, проверить, измерить и подтвердить. Если он не работает так, как задумано, тогда мы можем ответить на конкретные вопросы о том, как достичь ожидаемых результатов.

Майкл Шоу
источник
4
Это на самом деле не отвечает на все вопросы.
WhatsName
Вы, кажется, предполагаете, что существует ОДИН правильный путь. Я не думаю, что есть. Я знаю, что вы должны использовать его подходящим образом, затем протестировать и измерить его поведение, а затем корректировать свой подход, пока вы не будете счастливы.
Майкл Шоу
Вы немного написали о том, использовать ли HTTP или не общаться. ОП спросил о том, как лучше использовать тот или иной компонент библиотеки.
whatsisname
1
@MichaelShaw: HttpClientреализует IDisposable. Поэтому вполне разумно ожидать, что это будет недолговечный объект, который умеет убирать за собой, подходящий для включения в usingоператор каждый раз, когда он вам нужен. К сожалению, это не совсем так. Сообщение в блоге, на которое ссылается OP, ясно демонстрирует, что существуют ресурсы (в частности, соединения сокетов TCP), которые живут долго после того, как usingоператор вышел из области видимости, и HttpClientобъект предположительно был удален.
Роберт Харви
1
Я понимаю этот мыслительный процесс. Просто если бы вы думали об HTTP с точки зрения архитектуры и намеревались сделать много запросов к одному и тому же сервису - тогда вы подумали бы о кэшировании и конвейерной обработке, а затем мысль о том, чтобы сделать HttpClient недолговечным объектом, просто чувствую себя не так. Аналогично, если вы отправляете запросы на разные серверы и не получаете никакой выгоды от поддержания работоспособности сокета, тогда имеет смысл избавиться от объекта HttpClient после его использования.
Майкл Шоу,