Чи слід створити новий єдиний екземпляр HttpClient для всіх запитів?


57

нещодавно я натрапив на це повідомлення в блозі від монстрів asp.net, який розповідає про проблеми з використанням 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/ …
Joeri Sebrechts

(Як зазначено у моїй відповіді, я просто хотів зробити його більш помітним, тому я пишу короткий коментар.) Статичний екземпляр належним чином оброблятиме з'єднання tcp, закриваючи рукостискання, як тільки ви зробите Close()або ініціюєте нове Get(). Якщо ви просто розпоряджаєтесь клієнтом, коли закінчите з ним, не буде з ким обробляти це закриття рукостискання, і ваші порти будуть мати стан TIME_WAIT через це.
Младен Б.

Відповіді:


39

Це здається переконливою публікацією в блозі. Однак, перш ніж приймати рішення, я спершу запустив би ті самі тести, що і автор запису блогу, але за власним кодом. Я також спробував би дізнатися трохи більше про HttpClient та його поведінку.

У цій публікації зазначено:

Екземпляр HttpClient - це сукупність налаштувань, застосованих до всіх запитів, виконаних цим екземпляром. Крім того, кожен екземпляр HttpClient використовує власний пул з'єднань, ізолюючи свої запити від запитів, виконаних іншими екземплярами HttpClient.

Тож, що, ймовірно, відбувається при спільному використанні HttpClient, це те, що з'єднання використовуються повторно, що добре, якщо вам не потрібні постійні з'єднання. Єдиний спосіб ви точно знаєте, чи це має значення для вашої ситуації - це запустити власні тести на ефективність.

Якщо ви копаєте, ви знайдете кілька інших ресурсів, які вирішують цю проблему (включаючи статтю про кращі практики Microsoft), тому, мабуть, це було б корисно все-таки здійснити (з деякими запобіжними заходами).

Список літератури

Ви використовуєте Httpclient Неправильно, і це дестабілізує ваше програмне забезпечення
Singleton HttpClient? Остерігайтеся цієї серйозної поведінки та способів її виправлення: «
Шаблони та практики Microsoft» - Оптимізація продуктивності: Неправильна інстанція
Один екземпляр багаторазового використання HttpClient при перегляді коду
Singleton HttpClient не поважає зміни DNS (CoreFX)
Загальні поради щодо використання HttpClient


1
Це хороший обширний список. Це моє читання у вихідні.
Анкіт Віджай

"Якщо ви копаєте, ви знайдете кілька інших ресурсів, які вирішують цю проблему ..." Ви маєте на увазі сказати відкрите питання про з'єднання TCP?
Анкіт Віджай

Коротка відповідь: використовуйте статичний HttpClient . Якщо вам потрібно підтримувати зміни DNS (вашого веб-сервера чи інших серверів), вам потрібно потурбуватися про налаштування часу очікування.
Джесс

3
Це свідчить про те, як заплутався HttpClient, що його використання - це "читання на вихідних", як коментує @AnkitVijay.
usr

@Jess окрім змін у DNS - перекидання всього трафіку вашого клієнта через один сокет також зіпсує балансування навантаження?
Iain

16

Я спізнююсь на вечірку, але ось моя навчальна подорож на цю складну тему.

1. Де можна знайти офіційного захисника щодо повторного використання HttpClient?

Я маю на увазі, якщо планується повторне використання HttpClient, і це важливо , такий адвокат краще задокументувати у власній документації API, а не ховати у багатьох "Розширених темах", "Ефективності (анти) шаблоні" чи інших публікаціях у блозі . Інакше як новий студент повинен знати це ще до того, як пізно?

На сьогодні (травень 2018 р.) Перший результат пошуку, коли googling "c # httpclient" вказує на цю довідкову сторінку API в MSDN , яка взагалі не згадує про цей намір. Ну, урок 1 для новачків - завжди натискайте посилання "Інші версії" відразу після заголовка довідкової сторінки MSDN, ви, ймовірно, знайдете там посилання на "поточну версію". У цьому випадку HttpClient він перенесе вас до останнього документа, що містить опис цього наміру .

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

2. Помилкове поняття using IDisposable

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

Я вважаю, що це зводиться до невисловленого (неправильного?) Поняття: "Очікується, що об'єкт, що не має доступу, буде короткочасним" .

ЗАРАЗ, хоча це, звичайно, виглядає як недовговічна річ, коли ми пишемо код у такому стилі:

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

в офіційній документації на 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, що з'єднання залишається у встановленому стані, що означає, що воно має НЕ було належним чином закрито. Якби він був закритий, його стан був би замість TIME_WAIT. На практиці не дуже важливо протікати лише одне підключення після закінчення всієї вашої програми, а блог-плакат все ще бачить підвищення продуктивності після виправлення; але все-таки, концептуально неправильно звинувачувати IDisposable і вирішити НЕ розпоряджатися ним.

3. Чи потрібно ставити HttpClient у статичну властивість чи навіть ставити його як синглтон?

Виходячи з розуміння попереднього розділу, я думаю, що відповідь тут стає зрозумілою: "не обов'язково". Це дійсно залежить від того, як ви впорядковуєте свій код, доки ви повторно використовуєте HttpClient І (в ідеалі) розпоряджаєтесь ним з часом.

Весело, що навіть приклад у розділі « Зауваження» цього офіційного документа не робить це суворо правильно. Він визначає клас "GoodController", що містить статичну властивість HttpClient, яка не розпоряджається; що не дотримується того, що підкреслює інший приклад у розділі Приклади : "потрібно викликати розпорядження ... щоб додаток не просочував ресурси".

І, нарешті, одиночний не без власних викликів.

"Скільки людей вважають глобальну змінну гарною ідеєю? Ніхто.

Скільки людей думають, що одиночна справа - це гарна ідея? Декілька.

Що дає? Синглтони - це лише купа глобальних змінних ".

- Цитується з цієї натхненної бесіди "Глобальна держава та одинаки"

PS: SqlConnection

Цей питання не має значення для нинішніх запитань, але це, мабуть, добре знати. Шаблон використання SqlConnection відрізняється. Ви НЕ повинні повторно використовувати SqlConnection , тому що він буде обробляти свій пул з'єднань краще.

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

І вам все-таки потрібно розпоряджатися SqlConnection так само, як ви повинні зробити для HttpClient.


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 () та чекати, коли подія закриття з іншої сторони (FIN + ACK) надійде на наш бік. Тільки тоді ми можемо відправити наш остаточний ACK та розпоряджатися HttpClient.

Для додавання має сенс тримати відкриті з’єднання TCP, якщо ви виконуєте HTTP-запити, через заголовка HTTP "З'єднання: Тримайте-живий". Більше того, ви можете попросити віддаленого однорангового закрити з'єднання для вас, встановивши заголовок HTTP "З'єднання: Закрити". Таким чином, ваші локальні порти завжди будуть належним чином закриті, замість того, щоб вони знаходилися в 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
Це насправді не відповідає нічого запитуваному.
whatsisname

Ви ніби припускаєте, що існує ОДИН правильний шлях. Я не думаю, що існує. Я знаю, що ви повинні використовувати його відповідним чином, потім протестуйте і виміряйте, як він веде себе, а потім налаштуйте свій підхід, поки ви не будете щасливі.
Майкл Шоу

Ви трохи писали про те, використовувати HTTP чи ні для спілкування. ОП запитала про те, як найкраще використовувати певний компонент бібліотеки.
whatsisname

1
@MichaelShaw: HttpClientреалізує IDisposable. Тому нерозумно очікувати, що це короткочасний об’єкт, який вміє прибирати після себе, придатний для обгортання у usingзаяві кожного разу, коли це потрібно. На жаль, це не так, як це насправді працює. Повідомлення в блозі, пов'язане з ОП, наочно демонструє, що існують ресурси (зокрема, з'єднання сокетів TCP), які живуть довго після того, як usingзаява вийшла із сфери застосування та HttpClientоб'єкт, мабуть, був утилізований.
Роберт Харві

1
Я розумію той розумовий процес. Це просто, якщо ви думали про HTTP з точки зору архітектури і мали намір зробити багато запитів до однієї служби - тоді ви думали б про кешування та конвеєрне, а тоді думка зробити HttpClient короткоживим об'єктом просто почуватися не так. Так само, якщо ви звертаєтесь із запитами до різних серверів і не отримуєте ніякої користі від збереження сокета в прямому ефірі, то видалення об'єкта HttpClient після його використання має сенс.
Майкл Шоу
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.