410 Stimmen

Müssen HttpClient und HttpClientHandler zwischen Anfragen entsorgt werden?

System.Net.Http.HttpClient und System.Net.Http.HttpClientHandler in .NET Framework 4.5 implementieren IDisposable (über System.Net.Http.HttpMessageInvoker).

Die Dokumentation zum using Statement sagt:

Im Allgemeinen sollten Sie, wenn Sie ein IDisposable-Objekt verwenden, es in einem using-Statement deklarieren und instanziieren.

Diese Antwort verwendet dieses Muster:

var baseAddress = new Uri("http://example.com");
var cookieContainer = new CookieContainer();
using (var handler = new HttpClientHandler() { CookieContainer = cookieContainer })
using (var client = new HttpClient(handler) { BaseAddress = baseAddress })
{
    var content = new FormUrlEncodedContent(new[]
    {
        new KeyValuePair("foo", "bar"),
        new KeyValuePair("baz", "bazinga"),
    });
    cookieContainer.Add(baseAddress, new Cookie("CookieName", "cookie_value"));
    var result = client.PostAsync("/test", content).Result;
    result.EnsureSuccessStatusCode();
}

Aber die sichtbarsten Beispiele von Microsoft rufen Dispose() weder explizit noch implizit auf. Zum Beispiel:

In den Kommentaren der Ankündigung fragte jemand den Microsoft-Mitarbeiter:

Nach Überprüfung Ihrer Beispiele habe ich gesehen, dass Sie die Dispose-Aktion auf der HttpClient-Instanz nicht ausgeführt haben. Ich habe alle Instanzen von HttpClient mit dem using-Statement in meiner App verwendet und dachte, dass dies der richtige Weg ist, da HttpClient das IDisposable-Interface implementiert. Befinde ich mich auf dem richtigen Weg?

Seine Antwort war:

Im Allgemeinen ist das richtig, obwohl Sie bei "using" und async vorsichtig sein müssen, da sie in .NET 4 nicht wirklich zusammenpassen. In .NET 4.5 können Sie "await" innerhalb eines "using"-Statements verwenden.

Übrigens können Sie den gleichen HttpClient so oft wiederverwenden, wie Sie möchten, daher müssen Sie sie in der Regel nicht ständig erstellen/entsorgen.

Der zweite Absatz ist für diese Frage überflüssig, da es nicht darum geht, wie oft Sie eine HttpClient-Instanz verwenden können, sondern ob es notwendig ist, diese zu entsorgen, nachdem Sie sie nicht mehr benötigen.

(Update: In der Tat ist dieser zweite Absatz der Schlüssel zur Antwort, wie unten von @DPeden angegeben.)

Also meine Fragen sind:

  1. Ist es angesichts der aktuellen Implementierung (.NET Framework 4.5) erforderlich, Dispose() auf HttpClient- und HttpClientHandler-Instanzen aufzurufen? Klarstellung: Mit "erforderlich" meine ich, ob es negative Konsequenzen gibt, wenn Sie keine Entsorgung durchführen, wie beispielsweise Ressourcenlecks oder Risiken für Datenkorruption.

  2. Wenn es nicht erforderlich ist, wäre es trotzdem eine "gute Praxis", da sie IDisposable implementieren?

  3. Wenn es erforderlich (oder empfohlen) ist, implementiert der oben erwähnte Code dies sicher (für .NET Framework 4.5)?

  4. Wenn diese Klassen keine Dispose()-Aufrufe erfordern, warum wurden sie dann als IDisposable implementiert?

  5. Wenn sie erforderlich sind oder es eine empfohlene Praxis ist, sind die Microsoft-Beispiele irreführend oder unsicher?

10voto

Timothy Gonzalez Punkte 1514

Dispose() ruft den untenstehenden Code auf, der die Verbindungen schließt, die vom HttpClient-Objekt geöffnet wurden. Der Code wurde durch Dekompilieren mit dotPeek erstellt.

HttpClientHandler.cs - Dispose

ServicePointManager.CloseConnectionGroups(this.connectionGroupName);

Wenn Sie Dispose nicht aufrufen, wird ServicePointManager.MaxServicePointIdleTime, der durch einen Timer ausgeführt wird, die HTTP-Verbindungen schließen. Standardmäßig sind es 100 Sekunden.

ServicePointManager.cs

internal static readonly TimerThread.Callback s_IdleServicePointTimeoutDelegate = new TimerThread.Callback(ServicePointManager.IdleServicePointTimeoutCallback);
private static volatile TimerThread.Queue s_ServicePointIdlingQueue = TimerThread.GetOrCreateQueue(100000);

private static void IdleServicePointTimeoutCallback(TimerThread.Timer timer, int timeNoticed, object context)
{
  ServicePoint servicePoint = (ServicePoint) context;
  if (Logging.On)
    Logging.PrintInfo(Logging.Web, SR.GetString("net_log_closed_idle", (object) "ServicePoint", (object) servicePoint.GetHashCode()));
  lock (ServicePointManager.s_ServicePointTable)
    ServicePointManager.s_ServicePointTable.Remove((object) servicePoint.LookupString);
  servicePoint.ReleaseAllConnectionGroups();
}

Wenn Sie die Leerlaufzeit nicht auf unendlich gesetzt haben, scheint es sicher zu sein, Dispose nicht aufzurufen und den Idle-Verbindungs-Timer die Verbindungen für Sie schließen zu lassen. Es wäre jedoch besser, Dispose in einer using-Anweisung aufzurufen, wenn Sie wissen, dass Sie mit einer HttpClient-Instanz fertig sind und die Ressourcen schneller freigeben möchten.

5voto

David Faivre Punkte 2232

In meinem Fall habe ich einen HttpClient innerhalb einer Methode erstellt, die tatsächlich den Serviceaufruf durchführte. Etwas wie:

public void DoServiceCall() {
  var client = new HttpClient();
  await client.PostAsync();
}

In einer Azure-Arbeiterrolle würde nach wiederholtem Aufruf dieser Methode (ohne den HttpClient zu entsorgen) letztendlich ein Fehler mit SocketException (Verbindungsversuch fehlgeschlagen) auftreten.

Ich habe den HttpClient zu einer Instanzvariable gemacht (und ihn auf Klassenebene entsorgt), und das Problem verschwand. Also würde ich sagen, ja, entsorgen Sie den HttpClient, vorausgesetzt, es ist sicher (Sie haben keine ausstehenden asynchronen Aufrufe), dies zu tun.

3voto

Tom Deseyn Punkte 1705

Im typischen Gebrauch (Antworten<2GB) ist es nicht notwendig, die HttpResponseMessages zu entsorgen.

Die Rückgabetypen der HttpClient-Methoden sollten entsorgt werden, wenn ihr Stream-Inhalt nicht vollständig gelesen wird. Andernfalls gibt es keine Möglichkeit für den CLR zu wissen, dass diese Streams geschlossen werden können, bis sie garbage collected werden.

  • Wenn Sie die Daten in ein byte[] (z.B. GetByteArrayAsync) oder in einen String lesen, werden alle Daten gelesen, daher ist keine Entsorgung erforderlich.
  • Die anderen Überladungen werden standardmäßig den Stream bis zu 2GB lesen (HttpCompletionOption ist ResponseContentRead, HttpClient.MaxResponseContentBufferSize ist standardmäßig 2GB)

Wenn Sie HttpCompletionOption auf ResponseHeadersRead setzen oder die Antwort größer als 2GB ist, sollten Sie aufräumen. Dies kann durch Aufrufen von Dispose auf das HttpResponseMessage erfolgen oder durch Aufrufen von Dispose/Close auf den Stream, der aus dem HttpResonseMessage-Content erhalten wurde oder durch vollständiges Lesen des Inhalts.

Ob Sie Dispose auf den HttpClient aufrufen, hängt davon ab, ob Sie ausstehende Anfragen abbrechen möchten oder nicht.

2voto

TamusJRoyce Punkte 738

Wenn Sie HttpClient entsorgen möchten, können Sie dies tun, wenn Sie es als Ressourcenpool einrichten. Und am Ende Ihrer Anwendung entsorgen Sie Ihren Ressourcenpool.

Code:

// Beachten Sie, dass IDisposable hier nicht implementiert ist!
public interface HttpClientHandle
{
    HttpRequestHeaders DefaultRequestHeaders { get; }
    Uri BaseAddress { get; set; }
    // ...
    // Alle anderen Methoden von HttpClient
}

public class HttpClientHander : HttpClient, HttpClientHandle, IDisposable
{
    public static ConditionalWeakTable _httpClientsPool;
    public static HashSet _uris;

    static HttpClientHander()
    {
        _httpClientsPool = new ConditionalWeakTable();
        _uris = new HashSet();
        SetupGlobalPoolFinalizer();
    }

    private DateTime _delayFinalization = DateTime.MinValue;
    private bool _isDisposed = false;

    public static HttpClientHandle GetHttpClientHandle(Uri baseUrl)
    {
        HttpClientHander httpClient = _httpClientsPool.GetOrCreateValue(baseUrl);
        _uris.Add(baseUrl);
        httpClient._delayFinalization = DateTime.MinValue;
        httpClient.BaseAddress = baseUrl;

        return httpClient;
    }

    void IDisposable.Dispose()
    {
        _isDisposed = true;
        GC.SuppressFinalize(this);

        base.Dispose();
    }

    ~HttpClientHander()
    {
        if (_delayFinalization == DateTime.MinValue)
            _delayFinalization = DateTime.UtcNow;
        if (DateTime.UtcNow.Subtract(_delayFinalization) < base.Timeout)
            GC.ReRegisterForFinalize(this);
    }

    private static void SetupGlobalPoolFinalizer()
    {
        AppDomain.CurrentDomain.ProcessExit +=
            (sender, eventArgs) => { FinalizeGlobalPool(); };
    }

    private static void FinalizeGlobalPool()
    {
        foreach (var key in _uris)
        {
            HttpClientHander value = null;
            if (_httpClientsPool.TryGetValue(key, out value))
                try { value.Dispose(); } catch { }
        }

        _uris.Clear();
        _httpClientsPool = null;
    }
}

var handler = HttpClientHander.GetHttpClientHandle(new Uri("base url")).

  • HttpClient kann als Schnittstelle Dispose() nicht aufrufen.
  • Dispose() wird vom Garbage Collector verzögert aufgerufen. Oder wenn das Programm das Objekt durch seinen Destruktor aufräumt.
  • Verwendet schwache Referenzen + verzögerte Aufräumlogik, damit es so lange in Benutzung bleibt, wie es häufig wiederverwendet wird.
  • Es weist nur für jeden übergebenen Basis-URL einen neuen HttpClient zu. Gründe werden durch Ohad Schneider unten erläutert. Schlechtes Verhalten beim Ändern der Basis-URL.
  • HttpClientHandle ermöglicht Mocking in Tests

1voto

alastairtree Punkte 3535

Die Verwendung von Dependency Injection im Konstruktor erleichtert das Management der Lebensdauer Ihres HttpClient - sie verlagert das Lebenszyklusmanagement außerhalb des Codes, der es benötigt, und macht es leicht zu ändern zu einem späteren Zeitpunkt.

Meine aktuelle Präferenz besteht darin, eine separate Http-Client-Klasse zu erstellen, die einmal pro Ziel-Endpunktdomäne von HttpClient erbt und sie dann mithilfe der Dependency Injection zu einem Singleton zu machen. public class ExampleHttpClient: HttpClient { ... }

Dann nehme ich in den Serviceklassen, in denen ich auf diese API zugreifen muss, eine Konstruktorabhängigkeit auf den benutzerdefinierten Http-Client. Dies löst das Lebensdauerproblem und hat Vorteile beim Verbindungspooling.

Sie können ein Beispiel in der zugehörigen Antwort unter https://stackoverflow.com/a/50238944/3140853 finden.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X