Sollten wir für alle Anfragen eine neue einzelne Instanz von HttpClient erstellen?


57

Vor kurzem bin ich auf diesen Blog-Beitrag von asp.net-Monstern gestoßen, der über Probleme bei der Verwendung HttpClientauf folgende Weise spricht :

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

Laut dem Blog-Post HttpClientkönnen die TCP-Verbindungen offen bleiben , wenn wir die nach jeder Anfrage entsorgen . Dies kann möglicherweise dazu führen System.Net.Sockets.SocketException.

Der korrekte Weg laut Post ist, eine einzelne Instanz von zu erstellen, HttpClientda dies dazu beiträgt, die Verschwendung von Steckdosen zu reduzieren.

Von der Post:

Wenn wir eine einzelne Instanz von HttpClient gemeinsam nutzen, können wir die Verschwendung von Sockets reduzieren, indem wir sie wiederverwenden:

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

Ich habe HttpClientObjekte immer entsorgt , nachdem ich sie verwendet habe, da ich der Meinung war, dass dies der beste Weg ist, sie zu verwenden. Aber dieser Blog-Beitrag gibt mir das Gefühl, dass ich es die ganze Zeit falsch gemacht habe.

Sollten wir HttpClientfür alle Anfragen eine neue einzelne Instanz von erstellen ? Gibt es Fallstricke bei der Verwendung statischer Instanzen?


Haben Sie Probleme festgestellt, die Sie auf die Art und Weise zurückgeführt haben, wie Sie es verwenden?
Whatsisname

Vielleicht überprüfe diese Antwort und auch diese .
John Wu

@whatsisname nein, ich habe nicht, aber als ich mir den Blog ansah, hatte ich das Gefühl, dass ich das die ganze Zeit falsch benutze. Daher wollte ich von anderen Entwicklern wissen, ob sie in beiden Ansätzen ein Problem sehen.
Ankit Vijay

3
Ich habe es nicht selbst ausprobiert (daher nicht als Antwort), aber laut Microsoft ab .NET Core 2.1 sollten Sie HttpClientFactory verwenden, wie unter docs.microsoft.com/en-us/dotnet/standard/ beschrieben. …
Joeri Sebrechts

(Wie in meiner Antwort angegeben, wollte ich es nur sichtbarer machen, deshalb schreibe ich einen kurzen Kommentar.) Die statische Instanz wird den Handshake zum Schließen der TCP-Verbindung ordnungsgemäß handhaben, sobald Sie Close()einen neuen ausführen oder initiieren Get(). Wenn Sie den Client nur entsorgen, wenn Sie damit fertig sind, gibt es niemanden, der diesen schließenden Handshake handhabt, und Ihre Ports haben aus diesem Grund alle den Status TIME_WAIT.
Mladen B.

Antworten:


39

Es scheint wie ein überzeugender Blog-Beitrag. Bevor ich jedoch eine Entscheidung traf, führte ich zunächst dieselben Tests durch, die der Blogschreiber durchgeführt hatte, jedoch mit Ihrem eigenen Code. Ich würde auch versuchen, ein bisschen mehr über HttpClient und sein Verhalten herauszufinden.

In diesem Beitrag heißt es:

Eine HttpClient-Instanz ist eine Sammlung von Einstellungen, die auf alle von dieser Instanz ausgeführten Anforderungen angewendet werden. Darüber hinaus verwendet jede HttpClient-Instanz ihren eigenen Verbindungspool, wodurch ihre Anforderungen von Anforderungen isoliert werden, die von anderen HttpClient-Instanzen ausgeführt werden.

Wenn also ein HTTP-Client freigegeben wird, passiert wahrscheinlich, dass die Verbindungen wiederverwendet werden. Dies ist in Ordnung, wenn Sie keine dauerhaften Verbindungen benötigen. Der einzige Weg, um sicherzugehen, ob dies für Ihre Situation von Bedeutung ist oder nicht, besteht darin, Ihre eigenen Leistungstests durchzuführen.

Wenn Sie graben, finden Sie mehrere andere Ressourcen, die dieses Problem beheben (einschließlich eines Microsoft Best Practices-Artikels). Daher ist es wahrscheinlich eine gute Idee, sie trotzdem zu implementieren (mit einigen Vorsichtsmaßnahmen).

Verweise

Sie verwenden HttpClient Wrong und es destabilisiert Ihre Software
Singleton HttpClient? Beachten Sie dieses schwerwiegende Verhalten und beheben Sie es
Microsoft Patterns and Practices - Leistungsoptimierung: Fehlerhafte Instantiierung
Einzelne wiederverwendbare Instanz von HttpClient bei der
Codeüberprüfung Singleton HttpClient berücksichtigt keine DNS-Änderungen (CoreFX)
Allgemeine Hinweise zur Verwendung von HttpClient


1
Das ist eine gute umfangreiche Liste. Das ist mein Wochenende gelesen.
Ankit Vijay

"Wenn Sie graben, werden Sie mehrere andere Ressourcen finden, die dieses Problem beheben ..." Wollen Sie damit das Problem der offenen TCP-Verbindung sagen?
Ankit Vijay

Kurze Antwort: Verwenden Sie einen statischen HTTP-Client . Wenn Sie DNS-Änderungen (Ihres Webservers oder anderer Server) unterstützen müssen, müssen Sie sich über Timeout-Einstellungen Gedanken machen.
Jess

3
Es ist ein Beweis dafür, wie durcheinander HttpClient ist, dass es ein "Wochenendlesen" ist, wie von @AnkitVijay kommentiert.
USR

@Jess neben DNS-Änderungen - Wenn Sie den gesamten Datenverkehr Ihres Clients über einen einzigen Socket leiten, wird auch der Lastenausgleich durcheinander gebracht?
Iain

16

Ich komme zu spät zur Party, aber hier ist meine Lernreise zu diesem kniffligen Thema.

1. Wo finden wir den offiziellen Anwalt für die Wiederverwendung von HttpClient?

Ich meine, wenn die Wiederverwendung von HttpClient beabsichtigt ist und dies wichtig ist , ist ein solcher Befürworter besser in seiner eigenen API-Dokumentation dokumentiert, als in vielen "Advanced Topics", "Performance (anti) pattern" oder anderen Blog-Posts versteckt zu sein . Ansonsten, wie soll ein neuer Lernender es wissen, bevor es zu spät ist?

Ab sofort (Mai 2018) verweist das erste Suchergebnis beim Googeln von "c # httpclient" auf diese API-Referenzseite auf MSDN , die diese Absicht überhaupt nicht erwähnt. Nun, Lektion 1 hier für Anfänger ist, klicken Sie immer auf den Link "Andere Versionen" direkt nach der Überschrift der MSDN-Hilfeseite. Dort finden Sie wahrscheinlich Links zur "aktuellen Version". In diesem HttpClient-Fall werden Sie zum neuesten Dokument mit dieser Absichtsbeschreibung weitergeleitet .

Ich vermute, dass viele Entwickler, die neu in diesem Thema waren, auch nicht die richtige Dokumentationsseite gefunden haben. Deshalb ist dieses Wissen nicht weit verbreitet, und die Leute waren überrascht, als sie es später herausfanden , möglicherweise auf schwierige Weise .

2. Die (falsche?) Vorstellung von using IDisposable

Dies ist ein wenig vom Thema abweichend, aber es ist immer noch erwähnenswert, dass es kein Zufall ist, zu sehen, dass die Leute in diesen oben genannten Blog-Posts beschuldigen, dass HttpClientdie IDisposableBenutzeroberfläche dazu neigt, das using (var client = new HttpClient()) {...}Muster zu verwenden , und dann zum Problem führen.

Ich glaube, dass dies auf eine unausgesprochene (falsche?) Vorstellung zurückzuführen ist: "Es wird erwartet, dass ein IDisposable-Objekt von kurzer Dauer ist" .

JEDOCH, obwohl es sicherlich kurzlebig ist, wenn wir Code in diesem Stil schreiben:

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

Die offizielle Dokumentation zu IDisposable erwähnt niemals, dass IDisposableObjekte von kurzer Dauer sein müssen. IDisposable ist per Definition lediglich ein Mechanismus, mit dem Sie nicht verwaltete Ressourcen freigeben können. Nichts mehr. In diesem Sinne wird ERWARTET, dass Sie eventuell die Entsorgung auslösen, dies ist jedoch nicht von kurzer Dauer.

Es ist daher Ihre Aufgabe, den richtigen Zeitpunkt für die Entsorgung zu bestimmen, basierend auf den Anforderungen des Lebenszyklus Ihres realen Objekts. Nichts hindert Sie daran, ein IDisposable dauerhaft zu verwenden:

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

Mit diesem neuen Verständnis können wir jetzt, wenn wir diesen Blog-Beitrag erneut besuchen , deutlich feststellen, dass der "Fix" HttpClienteinmal initialisiert wird, ihn aber nie wieder freigibt. Aus diesem Grund können wir anhand seiner Netstat-Ausgabe erkennen, dass die Verbindung im Zustand ESTABLISHED verbleibt, was bedeutet, dass er besteht NICHT richtig geschlossen worden. Wenn es geschlossen wäre, wäre sein Status stattdessen TIME_WAIT. In der Praxis ist es keine große Sache, nach dem Ende Ihres gesamten Programms nur eine Verbindung zu verlieren, und das Blog-Poster kann nach dem Fix noch einen Leistungsgewinn verzeichnen. Dennoch ist es konzeptionell falsch, IDisposable die Schuld zu geben und sich dafür zu entscheiden, NICHT darüber zu verfügen.

3. Müssen wir HttpClient in eine statische Eigenschaft oder sogar als Singleton setzen?

Basierend auf dem Verständnis des vorherigen Abschnitts wird die Antwort hier meines Erachtens klar: "nicht unbedingt". Es hängt wirklich davon ab, wie Sie Ihren Code organisieren, solange Sie einen HTTP-Client wiederverwenden UND (im Idealfall) irgendwann entsorgen.

Komischerweise stimmt das Beispiel im Abschnitt "Bemerkungen" des aktuellen offiziellen Dokuments nicht ganz. Es definiert eine "GoodController" -Klasse, die eine statische HttpClient-Eigenschaft enthält, die nicht entsorgt wird. was nicht gehorcht, was ein anderes Beispiel im Abschnitt " Beispiele" betont: "muss entsorgen ... damit die App keine Ressourcen verliert ".

Und schließlich ist Singleton nicht ohne eigene Herausforderungen.

"Wie viele Leute halten globale Variablen für eine gute Idee? Niemand.

Wie viele Leute halten Singleton für eine gute Idee? Ein paar.

Was gibt? Singletons sind nur ein paar globale Variablen. "

- Zitiert aus diesem inspirierenden Vortrag "Global State and Singletons"

PS: SqlConnection

Dies ist für die aktuelle Frage und Antwort irrelevant, aber es ist wahrscheinlich gut zu wissen. Das Verwendungsmuster von SqlConnection ist unterschiedlich. Sie müssen SqlConnection NICHT erneut verwenden , da es den Verbindungspool auf diese Weise besser handhabt .

Der Unterschied ergibt sich aus dem Implementierungsansatz. Jede HttpClient-Instanz verwendet ihren eigenen Verbindungspool (von hier aus zitiert ). aber SqlConnection selbst wird von einem zentralen Verbindungspool verwaltet werden , nach diesem .

Und Sie müssen SqlConnection weiterhin wie für HttpClient entsorgen.


14

Ich habe einige Tests durchgeführt, bei denen Leistungsverbesserungen mit statischer Aufladung festgestellt wurden HttpClient. Ich habe folgenden Code für meine Tests verwendet:

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

Zum Prüfen:

  • Ich habe den Code mit 10, 100, 1000 und 1000 Verbindungen ausgeführt.
  • Führen Sie jeden Test dreimal durch, um den Durchschnitt zu ermitteln.
  • Es wird jeweils eine Methode ausgeführt

Ich fand die Leistungsverbesserung zwischen 40% und 60% bei Verwendung der statischen Aufladung, HttpClientanstatt sie für eine HttpClientAnfrage zu entsorgen . Ich habe die Details des Leistungstestergebnisses in den Blog-Beitrag hier gestellt .


1

Um die TCP-Verbindung ordnungsgemäß zu schließen , müssen Sie eine FIN-FIN + ACK-ACK-Paketsequenz ausführen (genau wie SYN-SYN + ACK-ACK, wenn Sie eine TCP-Verbindung herstellen ). Wenn wir nur eine .Close () - Methode aufrufen (normalerweise, wenn ein HttpClient verfügbar ist) und nicht darauf warten, dass die Remote-Seite unsere Anforderung zum Schließen bestätigt (mit FIN + ACK), wird der TIME_WAIT-Status aktiviert den lokalen TCP-Port, da wir unseren Listener (HttpClient) entsorgt haben und nie die Möglichkeit hatten, den Portstatus auf einen ordnungsgemäßen geschlossenen Status zurückzusetzen, sobald der Remote-Peer uns das FIN + ACK-Paket gesendet hat.

Der richtige Weg, die TCP-Verbindung zu schließen, besteht darin, die .Close () -Methode aufzurufen und zu warten, bis das Close-Ereignis von der anderen Seite (FIN + ACK) auf unserer Seite eintrifft. Erst dann können wir unser endgültiges ACK senden und den HttpClient entsorgen.

Nur zum Hinzufügen ist es sinnvoll, TCP-Verbindungen offen zu halten, wenn Sie HTTP-Anforderungen ausführen, da der HTTP-Header "Connection: Keep-Alive" verwendet wird. Außerdem können Sie die Gegenstelle auffordern, die Verbindung für Sie zu trennen, indem Sie den HTTP-Header "Connection: Close" setzen. Auf diese Weise werden Ihre lokalen Ports immer ordnungsgemäß geschlossen, anstatt sich in einem TIME_WAIT-Status zu befinden.


1

Hier ist ein grundlegender API-Client, der den HttpClient und den HttpClientHandler effizient verwendet. Wenn Sie einen neuen HttpClient erstellen, um eine Anfrage zu stellen, entsteht viel Overhead. Erstellen Sie HttpClient NICHT für jede Anforderung neu. HttpClient so oft wie möglich wiederverwenden ...

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

Verwendungszweck:

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

Es gibt keine Möglichkeit, die HttpClient-Klasse zu verwenden. Der Schlüssel besteht darin, Ihre Anwendung so zu gestalten, dass sie für die Umgebung und die Einschränkungen sinnvoll ist.

HTTP ist ein großartiges Protokoll, wenn Sie öffentliche APIs verfügbar machen müssen. Es kann auch effektiv für leichte interne Dienste mit geringer Latenz verwendet werden - obwohl das Muster der RPC-Nachrichtenwarteschlange für interne Dienste häufig die bessere Wahl ist.

Es ist sehr komplex, HTTP gut zu machen.

Folgendes berücksichtigen:

  1. Das Erstellen eines Sockets und das Herstellen einer TCP-Verbindung verbraucht Netzwerkbandbreite und -zeit.
  2. HTTP / 1.1 unterstützt Pipeline-Anforderungen auf demselben Socket. Mehrere Anfragen nacheinander senden, ohne auf die vorherigen Antworten warten zu müssen - dies ist wahrscheinlich für die vom Blog-Beitrag gemeldete Geschwindigkeitsverbesserung verantwortlich.
  3. Caching und Load Balancer - Wenn Sie einen Load Balancer vor den Servern haben, können Sie die Belastung Ihrer Server verringern, indem Sie sicherstellen, dass Ihre Anforderungen über geeignete Cache-Header verfügen, und die Antworten schneller an die Clients senden.
  4. Fragen Sie niemals eine Ressource ab, sondern verwenden Sie HTTP-Chunking, um regelmäßige Antworten zurückzugeben.

Vor allem aber testen, messen und bestätigen. Wenn es sich nicht wie geplant verhält, können wir spezifische Fragen beantworten, wie Sie Ihre erwarteten Ergebnisse erzielen können.


4
Dies beantwortet eigentlich nichts gefragt.
Whatsisname

Sie scheinen anzunehmen, dass es EINEN richtigen Weg gibt. Ich glaube nicht, dass es das gibt. Ich weiß, Sie müssen es in der angemessenen Weise verwenden, dann testen und messen, wie es sich verhält, und dann Ihren Ansatz anpassen, bis Sie zufrieden sind.
Michael Shaw

Sie haben ein wenig darüber geschrieben, ob Sie HTTP für die Kommunikation verwenden sollen oder nicht. Das OP fragte, wie eine bestimmte Bibliothekskomponente am besten verwendet werden könne.
Whatsisname

1
@MichaelShaw: HttpClientimplementiert IDisposable. Es ist daher nicht unangemessen, davon auszugehen, dass es sich um ein kurzlebiges Objekt handelt, das es versteht, nach sich selbst aufzuräumen, und das usingjedes Mal, wenn Sie es benötigen, in eine Anweisung eingebunden werden kann. Leider funktioniert es nicht so. Der vom OP verknüpfte Blogeintrag zeigt deutlich, dass es Ressourcen gibt (insbesondere TCP-Socket-Verbindungen), die lange nach dem usingErlöschen des Gültigkeitsbereichs der Anweisung und der HttpClientmutmaßlichen Entsorgung des Objekts weiterleben .
Robert Harvey

1
Ich verstehe diesen Denkprozess. Es ist nur so, wenn Sie aus der Sicht der Architektur über HTTP nachdenken und viele Anfragen an denselben Service stellen möchten - dann würden Sie über Caching und Pipelining nachdenken und dann überlegen, HttpClient zu einem kurzlebigen Objekt zu machen fühle mich einfach falsch. Wenn Sie Anforderungen an verschiedene Server stellen und keinen Nutzen daraus ziehen würden, den Socket am Leben zu halten, ist es ebenfalls sinnvoll, das HttpClient-Objekt nach seiner Verwendung zu entsorgen.
Michael Shaw
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.