Dovremmo creare una nuova singola istanza di HttpClient per tutte le richieste?


58

recentemente mi sono imbattuto in questo post sul blog di mostri asp.net che parla di problemi con l'utilizzo HttpClientnel modo seguente:

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

Come per il post sul blog, se disponiamo il HttpClientdopo ogni richiesta può mantenere aperte le connessioni TCP. Questo può potenzialmente portare a System.Net.Sockets.SocketException.

Il modo corretto secondo il post è quello di creare una singola istanza HttpClientin quanto aiuta a ridurre lo spreco di socket.

Dal post:

Se condividiamo una singola istanza di HttpClient, possiamo ridurre lo spreco di socket riutilizzandoli:

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();
        }
    }
}

Ho sempre smaltito l' HttpClientoggetto dopo averlo usato perché pensavo che questo fosse il modo migliore di usarlo. Ma questo post sul blog ora mi fa sentire che stavo sbagliando da così tanto tempo.

Dovremmo creare una nuova singola istanza di HttpClientper tutte le richieste? Ci sono insidie ​​nell'uso dell'istanza statica?


Hai riscontrato problemi che hai attribuito al modo in cui lo stai utilizzando?
whatsisname

Forse controlla questa risposta e anche questa .
John Wu,

@whatsisname no Non l'ho fatto, ma guardando il blog ho avuto la sensazione che potrei usarlo sempre in modo sbagliato. Quindi, volevo capire dai colleghi sviluppatori se vedono qualche problema in entrambi i metodi.
Ankit Vijay,

3
Non l'ho provato da solo (quindi non fornendo questo come risposta), ma secondo Microsoft a partire da .NET Core 2.1 dovresti usare HttpClientFactory come descritto su docs.microsoft.com/en-us/dotnet/standard/ ...
Joeri Sebrechts,

(Come affermato nella mia risposta, volevo solo renderlo più visibile, quindi sto scrivendo un breve commento.) L'istanza statica gestirà correttamente l'handshake di chiusura della connessione tcp, una volta che hai fatto Close()o hai iniziato una nuova Get(). Se hai appena eliminato il client quando hai finito, non ci sarà nessuno a gestire quella stretta di mano di chiusura e le tue porte avranno tutti lo stato TIME_WAIT, a causa di ciò.
Mladen B.

Risposte:


40

Sembra un post sul blog avvincente. Tuttavia, prima di prendere una decisione, vorrei prima eseguire gli stessi test eseguiti dallo scrittore del blog, ma con il tuo codice. Vorrei anche provare a scoprire qualcosa in più su HttpClient e il suo comportamento.

Questo post afferma:

Un'istanza di HttpClient è una raccolta di impostazioni applicate a tutte le richieste eseguite da tale istanza. Inoltre, ogni istanza di HttpClient utilizza il proprio pool di connessioni, isolando le sue richieste dalle richieste eseguite da altre istanze di HttpClient.

Quindi ciò che sta probabilmente accadendo quando un HttpClient è condiviso è che le connessioni vengono riutilizzate, il che va bene se non si richiedono connessioni permanenti. L'unico modo per sapere con certezza se questo è importante per la tua situazione è eseguire i tuoi test delle prestazioni.

Se scavi, troverai diverse altre risorse che risolvono questo problema (incluso un articolo sulle best practice di Microsoft), quindi è probabilmente una buona idea implementare comunque (con alcune precauzioni).

Riferimenti

Stai usando Httpclient sbagliato e sta destabilizzando il tuo software
Singleton HttpClient? Fai attenzione a questo comportamento grave e a come risolverlo
Schemi e pratiche Microsoft - Ottimizzazione delle prestazioni: istanziazione impropria
Singola istanza di HttpClient riutilizzabile sulla revisione del codice
Singleton HttpClient non rispetta le modifiche DNS (CoreFX)
Consigli generali per l'utilizzo di HttpClient


1
Questa è una buona lista estesa. Questo è il mio fine settimana letto.
Ankit Vijay,

"Se scavi, troverai diverse altre risorse che risolvono questo problema ..." intendi dire il problema di apertura della connessione TCP?
Ankit Vijay,

Risposta breve: utilizzare un HttpClient statico . Se è necessario supportare le modifiche DNS (del server Web o di altri server), è necessario preoccuparsi delle impostazioni di timeout.
Jess,

3
È una testimonianza di quanto HttpClient sia incasinato nel fatto che utilizzarlo è una "lettura del fine settimana", come commentato da @AnkitVijay.
usr

@Jess oltre alle modifiche al DNS - l'inoltro di tutto il traffico del tuo client attraverso un singolo socket incasinerà anche il bilanciamento del carico?
Iain,

16

Sono in ritardo alla festa, ma ecco il mio viaggio di apprendimento su questo argomento difficile.

1. Dove possiamo trovare l'avvocato ufficiale sul riutilizzo di HttpClient?

Voglio dire, se il riutilizzo di HttpClient è previsto e farlo è importante , tale avvocato è meglio documentato nella propria documentazione API, piuttosto che essere nascosto in molti "Argomenti avanzati", "Performance (anti) pattern" o altri post di blog là fuori . Altrimenti, come dovrebbe sapere un nuovo studente prima che sia troppo tardi?

A partire da ora (maggio 2018), il primo risultato di ricerca quando si cerca su Google "c # httpclient" punta a questa pagina di riferimento dell'API su MSDN , che non menziona affatto tale intenzione. Bene, la lezione 1 qui per i principianti è, fai sempre clic sul link "Altre versioni" subito dopo il titolo della pagina di aiuto di MSDN, probabilmente troverai i collegamenti alla "versione corrente" lì. In questo caso HttpClient, ti porterà all'ultimo documento qui contenente quella descrizione dell'intenzione .

Ho il sospetto che molti sviluppatori che erano nuovi a questo argomento non abbiano trovato la pagina di documentazione corretta, ecco perché questa conoscenza non è ampiamente diffusa e le persone sono state sorprese quando l'hanno scoperto in seguito , forse in modo difficile .

2. La (mis?) Concezione di using IDisposable

Questo è un po 'fuori tema, ma vale comunque la pena sottolineare che non è una coincidenza vedere le persone nei suddetti post di blog incolpare il modo in cui HttpClientl' IDisposableinterfaccia fa sì che tendano a utilizzare il using (var client = new HttpClient()) {...}modello e quindi a causare il problema.

Credo che si tratti di una concezione non detta (mis?): "Un oggetto IDisposable dovrebbe essere di breve durata" .

TUTTAVIA, mentre certamente sembra una cosa di breve durata quando scriviamo codice in questo stile:

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

la documentazione ufficiale su IDisposable non menziona mai IDisposableoggetti di breve durata. Per definizione, IDisposable è semplicemente un meccanismo che consente di rilasciare risorse non gestite. Niente di più. In tal senso, sei ASPETTATO di innescare lo smaltimento, ma non è necessario che tu lo faccia in un modo di breve durata.

È quindi tuo compito scegliere correttamente quando attivare lo smaltimento, in base ai requisiti del ciclo di vita dell'oggetto reale. Non c'è nulla che ti impedisca di utilizzare un IDisposable in modo longevo:

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();
        }
    }
}

Con questa nuova comprensione, ora rivisitiamo quel post sul blog , possiamo notare chiaramente che la "correzione" si inizializza HttpClientuna volta ma non la smaltisce mai, ecco perché possiamo vedere dal suo output netstat che, la connessione rimane allo stato STABILITO che significa che ha NON è stato chiuso correttamente. Se fosse chiuso, il suo stato sarebbe invece in TIME_WAIT. In pratica, non è un grosso problema perdere una sola connessione aperta al termine dell'intero programma e il poster del blog vede ancora un miglioramento delle prestazioni dopo la correzione; ma è concettualmente errato incolpare IDisposable e scegliere di NON smaltirlo.

3. Dobbiamo mettere HttpClient in una proprietà statica o persino metterlo come singleton?

Sulla base della comprensione della sezione precedente, penso che la risposta qui sia chiara: "non necessariamente". Dipende davvero da come organizzi il tuo codice, purché riutilizzi un HttpClient e (idealmente) lo elimini alla fine.

Esilarante, nemmeno l'esempio nella sezione Osservazioni del presente documento ufficiale fa perfettamente ragione. Definisce una classe "GoodController", contenente una proprietà HttpClient statica che non verrà eliminata; che disobbedisce a quanto enfatizza un altro esempio nella sezione Esempi : "è necessario chiamare dispose ... quindi l'app non perde risorse".

E, infine, singleton non è privo di sfide.

"Quante persone pensano che la variabile globale sia una buona idea? Nessuno.

Quante persone pensano che singleton sia una buona idea? Alcune.

Cosa dà? I singoli sono solo un gruppo di variabili globali ".

- Citato da questo discorso stimolante, "Global State and Singletons"

PS: SqlConnection

Questo è irrilevante per le attuali domande e risposte, ma è probabilmente una buona conoscenza. Il modello di utilizzo di SqlConnection è diverso. È NON è necessario riutilizzare SqlConnection , perché in grado di gestire il suo pool di connessioni meglio così.

La differenza è causata dal loro approccio all'implementazione. Ogni istanza di HttpClient utilizza il proprio pool di connessioni (citato da qui ); ma SqlConnection stesso è gestito da un pool di connessioni centrale, secondo questo .

E devi ancora smaltire SqlConnection, come si suppone faccia per HttpClient.


14

Ho fatto alcuni test per vedere i miglioramenti delle prestazioni con statico HttpClient. Ho usato sotto il codice per i miei test:

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);
            }
        }
    }
}

Per i test:

  • Ho eseguito il codice con 10, 100, 1000 e 1000 connessioni.
  • Ho eseguito ogni test 3 volte per scoprire la media.
  • Eseguito un metodo alla volta

Ho trovato il miglioramento delle prestazioni tra il 40% e il 60% su uon usando statico HttpClientinvece di smaltirlo per HttpClientrichiesta. Ho inserito i dettagli del risultato del test delle prestazioni nel post del blog qui .


1

Per chiudere correttamente la connessione TCP , è necessario completare una sequenza di pacchetti FIN - FIN + ACK - ACK (proprio come SYN - SYN + ACK - ACK, quando si apre una connessione TCP ). Se chiamiamo semplicemente un metodo .Close () (di solito accade quando un HttpClient sta eliminando ) e non aspettiamo che il lato remoto confermi la nostra richiesta di chiusura (con FIN + ACK), finiamo con lo stato TIME_WAIT su la porta TCP locale, perché abbiamo eliminato il nostro listener (HttpClient) e non abbiamo mai avuto la possibilità di ripristinare lo stato della porta su uno stato chiuso corretto, una volta che il peer remoto ci ha inviato il pacchetto FIN + ACK.

Il modo corretto di chiudere la connessione TCP sarebbe quello di chiamare il metodo .Close () e attendere che l'evento di chiusura dall'altra parte (FIN + ACK) arrivi dalla nostra parte. Solo così possiamo inviare il nostro ACK finale e smaltire HttpClient.

Solo per aggiungere, ha senso mantenere aperte le connessioni TCP, se si stanno eseguendo richieste HTTP, a causa dell'intestazione HTTP "Connessione: Keep-Alive". Inoltre, potresti chiedere al peer remoto di chiudere la connessione per te, invece, impostando l'intestazione HTTP "Connessione: Chiudi". In questo modo, le porte locali verranno sempre chiuse correttamente, anziché essere in uno stato TIME_WAIT.


1

Ecco un client API di base che utilizza HttpClient e HttpClientHandler in modo efficiente. Quando si crea un nuovo HttpClient per effettuare una richiesta, si verificano molte spese generali. NON ricreare HttpClient per ogni richiesta. Riutilizzare HttpClient il più possibile ...

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 + "/";
    }
}

Uso:

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

Non esiste un modo per utilizzare la classe HttpClient. La chiave è progettare l'applicazione in modo sensato per l'ambiente e i vincoli.

HTTP è un ottimo protocollo da utilizzare quando è necessario esporre le API pubbliche. Può anche essere utilizzato in modo efficace per servizi interni leggeri a bassa latenza, sebbene il modello di coda dei messaggi RPC sia spesso una scelta migliore per i servizi interni.

C'è molta complessità nel fare bene l'HTTP.

Considera quanto segue:

  1. La creazione di un socket e la creazione di una connessione TCP utilizzano larghezza di banda e tempo di rete.
  2. HTTP / 1.1 supporta le richieste di pipeline sullo stesso socket. Inviare più richieste una dopo l'altra, senza dover attendere le risposte precedenti - questo è probabilmente responsabile del miglioramento della velocità riportato dal post sul Blog.
  3. Memorizzazione nella cache e bilanciamento del carico: se si dispone di un bilanciamento del carico davanti ai server, assicurarsi che le richieste dispongano di intestazioni della cache appropriate può ridurre il carico sui server e ottenere le risposte ai client più rapidamente.
  4. Non eseguire mai il polling di una risorsa, utilizzare HTTP chunking per restituire risposte periodiche.

Ma soprattutto, prova, misura e conferma. Se non si comporta come previsto, possiamo rispondere a domande specifiche su come raggiungere i risultati previsti.


4
Questo in realtà non risponde a nessuna domanda.
whatsisname

Sembra supporre che esista UN UNICO modo corretto. Non penso che ci sia. So che devi usarlo nel modo appropriato, quindi testare e misurare come si comporta, quindi adattare il tuo approccio fino a quando non sei felice.
Michael Shaw,

Hai scritto un po 'sull'uso dell'uso dell'HTTP o meno per comunicare. L'OP ha chiesto come utilizzare al meglio un particolare componente della libreria.
whatsisname

1
@MichaelShaw: HttpClientimplementa IDisposable. Non è quindi irragionevole aspettarsi che sia un oggetto di breve durata che sappia ripulire dopo se stesso, adatto per essere racchiuso in usingun'affermazione ogni volta che ne hai bisogno. Sfortunatamente, non è così che funziona davvero. Il post sul blog che l'OP ha collegato dimostra chiaramente che ci sono risorse (in particolare connessioni socket TCP) che sopravvivono a lungo dopo che l' usingistruzione è uscita dall'ambito e HttpClientpresumibilmente l' oggetto è stato eliminato.
Robert Harvey,

1
Capisco quel processo di pensiero. È solo se pensavi all'HTTP dal punto di vista dell'architettura e avevi intenzione di fare molte richieste allo stesso servizio - allora avresti pensato alla cache e al pipelining, e quindi l'idea di rendere HttpClient un oggetto di breve durata sarebbe semplicemente sento male. Allo stesso modo, se si stanno effettuando richieste a server diversi e non si ottiene alcun vantaggio dal mantenere attivo il socket, è opportuno eliminare l'oggetto HttpClient dopo il suo utilizzo.
Michael Shaw,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.