← Zurück zum Blog

Proxys in .NET HttpClient: SocketsHttpHandler, SOCKS5 und Socket-Rotation ohne Leckagen

Wir erklären, wie man einen Proxy im .NET HttpClient mit SocketsHttpHandler richtig konfiguriert, SOCKS5 anschließt und die IP-Rotation ohne Socket-Erschöpfung organisiert.

📅18. Oktober 2026

Wenn Sie jemals SocketException: Address already in use erhalten haben oder gesehen haben, wie einer .NET-Anwendung plötzlich die verfügbaren Sockets nach ein paar Stunden Proxy-Nutzung ausgehen, liegt das Problem fast immer im falschen Gebrauch von HttpClient. In diesem Artikel werden wir besprechen, wie man Proxys über SocketsHttpHandler konfiguriert, SOCKS5 verbindet und IP-Rotation organisiert, sodass der Verbindungspool nicht zum Engpass wird.

Warum HttpClient Sockets „frisst“

Ein klassischer Fehler ist es, für jede Anfrage innerhalb von using eine neue Instanz von HttpClient zu erstellen. Bei der Arbeit mit Proxys wird dieses Problem verschärft: Jede Verbindung über einen Proxy-Server erfordert einen separaten TCP-Handshake, und wenn Sie den Proxy auch noch zur Laufzeit ändern, wird der Verbindungspool nicht wiederverwendet, und Sockets hängen im Zustand TIME_WAIT.

Unter Windows ist dies besonders kritisch — standardmäßig steht eine begrenzte Anzahl dynamischer Ports zur Verfügung (normalerweise 16384), und wenn die Anwendung Daten über einen Proxy mit hoher Anfrageintensität parst, können alle Ports in wenigen Minuten erschöpft sein. Unter Linux ist die Situation aufgrund der Kernel-Einstellungen etwas milder, aber das Problem bleibt bestehen, wenn Verbindungen nicht wiederverwendet werden.

Der alte HttpClientHandler, der seit den Zeiten des .NET Framework verfügbar ist, bietet keine flexible Kontrolle über den Lebenszyklus von Verbindungen. Aus diesem Grund wird seit .NET Core 2.1 empfohlen, SocketsHttpHandler zu verwenden — er ist vollständig in verwaltetem Code geschrieben und bietet direkten Zugriff auf den Verbindungspool, DNS-Timeouts und Proxy-Einstellungen.

SocketsHttpHandler: Wozu ist er gut

SocketsHttpHandler ist die Transportschicht, auf der seit .NET Core 2.1 HttpClient standardmäßig unter Linux und macOS basiert, und seit .NET 5 auch unter Windows. Im Gegensatz zum alten WinHttpHandler:

  • erlaubt es, PooledConnectionLifetime festzulegen — die Lebensdauer einer Verbindung im Pool, was bei der Arbeit mit rotierenden Proxys entscheidend ist;
  • bietet Zugriff auf IWebProxy über den Delegaten Proxy, der programmatisch zur Laufzeit überschrieben werden kann;
  • unterstützt ConnectCallback — einen Low-Level-Hook zum Ersetzen des Transports, der für SOCKS5 erforderlich ist;
  • gibt Sockets korrekt nach Ablauf von PooledConnectionIdleTimeout frei, ohne Müll im System zu hinterlassen.

Die grundlegende Konfiguration sieht so aus:

var handler = new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromMinutes(5),
    PooledConnectionIdleTimeout = TimeSpan.FromSeconds(30),
    MaxConnectionsPerServer = 50,
    AutomaticDecompression = DecompressionMethods.All
};

var client = new HttpClient(handler)
{
    Timeout = TimeSpan.FromSeconds(30)
};

Die wichtigste Regel: HttpClient mit diesem Handler sollte ein Singleton sein oder über IHttpClientFactory erstellt werden — erstellen Sie ihn nicht für jede Anfrage neu, sonst wird der gesamte Sinn des Verbindungspools zunichtegemacht.

Proxy-Konfiguration über SocketsHttpHandler

Für HTTP/HTTPS-Proxys reicht es aus, die Proxy-Eigenschaft auf der Handler-Ebene festzulegen. Dies funktioniert für Rechenzentrumsproxys und Residential-Proxys, die den Zugriff über das HTTP CONNECT-Protokoll bereitstellen:

var proxy = new WebProxy("http://proxy-host:8000")
{
    Credentials = new NetworkCredential("login", "password")
};

var handler = new SocketsHttpHandler
{
    Proxy = proxy,
    UseProxy = true,
    PooledConnectionLifetime = TimeSpan.FromMinutes(3)
};

var client = new HttpClient(handler);

var response = await client.GetAsync("https://example.com");
Console.WriteLine(await response.Content.ReadAsStringAsync());

Wenn Sie verschiedene Proxys für verschiedene Hosts anschließen müssen (z. B. erfolgt die Authentifizierung zu einer Domain direkt, während das Scraping über einen Proxy erfolgt), verwenden Sie einen Delegaten anstelle eines statischen Objekts:

var handler = new SocketsHttpHandler
{
    Proxy = new WebProxy(),
    UseProxy = true
};

handler.Proxy = new CustomProxy(uri =>
{
    return uri.Host.Contains("target-site")
        ? new Uri("http://proxy-host:8000")
        : null;
});

Für Aufgaben wie das Parsen von Marktplätzen — beispielsweise die Preisüberwachung auf Wildberries oder Ozon — kombinieren viele beide Ansätze: Der Hauptverkehr läuft über Rechenzentrumsproxys wie über Rechenzentrumsproxys, während Anfragen an Captcha-Dienste oder zur Authentifizierung direkt erfolgen.

SOCKS5-Unterstützung in .NET

Seit .NET 6 hat SocketsHttpHandler native Unterstützung für SOCKS4/SOCKS4a/SOCKS5 durch Angabe des Schemas in der Proxy-URI erhalten — keine externen Bibliotheken sind erforderlich:

var handler = new SocketsHttpHandler
{
    Proxy = new WebProxy(new Uri("socks5://proxy-host:1080"))
    {
        Credentials = new NetworkCredential("login", "password")
    },
    UseProxy = true
};

var client = new HttpClient(handler);
var response = await client.GetAsync("https://httpbin.org/ip");

Ein wichtiger Punkt: Bis .NET 6 gab es diese Möglichkeit nicht, und Entwickler mussten ihren eigenen ConnectCallback schreiben, der manuell das SOCKS5-Handschlagverfahren vor der TLS-Verbindung durchführte. Wenn Ihr Projekt noch auf .NET Core 3.1 oder .NET 5 basiert, ist dieser Weg immer noch relevant:

var handler = new SocketsHttpHandler
{
    ConnectCallback = async (context, cancellationToken) =>
    {
        var socket = new Socket(SocketType.Stream, ProtocolType.Tcp)
        {
            NoDelay = true
        };

        await socket.ConnectAsync("proxy-host", 1080, cancellationToken);
        await PerformSocks5Handshake(socket, context.DnsEndPoint, cancellationToken);

        return new NetworkStream(socket, ownsSocket: true);
    }
};

In der Praxis ist es für Produktionsprojekte einfacher, auf .NET 6+ zu aktualisieren und die integrierte Implementierung zu verwenden — sie ist im Runtime-Test abgedeckt und erfordert keine eigene Unterstützung des Handshake-Protokolls.

Proxy-Rotation ohne Neuschaffung von HttpClient

Ein häufiger Fehler bei der Implementierung der IP-Rotation ist die Erstellung eines neuen HttpClient (und damit eines neuen Handlers und eines neuen Verbindungspools) bei jedem Wechsel des Proxys. Dies führt garantiert zur Ansammlung nicht geschlossener Sockets. Der richtige Ansatz besteht darin, einen eigenen IWebProxy zu implementieren, der bei jedem Aufruf eine andere Proxy-Adresse zurückgibt:

public class RotatingProxy : IWebProxy
{
    private readonly string[] _proxies;
    private int _index = -1;

    public RotatingProxy(string[] proxies) => _proxies = proxies;

    public ICredentials Credentials { get; set; }

    public Uri GetProxy(Uri destination)
    {
        var next = Interlocked.Increment(ref _index) % _proxies.Length;
        return new Uri(_proxies[next]);
    }

    public bool IsBypassed(Uri host) => false;
}

Diese Klasse wird an denselben einzigen SocketsHttpHandler angeschlossen:

var handler = new SocketsHttpHandler
{
    Proxy = new RotatingProxy(new[]
    {
        "http://proxy1:8000",
        "http://proxy2:8000",
        "http://proxy3:8000"
    }),
    UseProxy = true,
    PooledConnectionLifetime = TimeSpan.FromSeconds(60)
};

var client = new HttpClient(handler);

Dieses Schema verwendet den internen Socket-Pool wieder, aber jede Anfrage erfolgt tatsächlich über eine neue IP. Wenn Sie parallele Verbindungspools für jeden Proxy (zur Geschwindigkeit) aufbauen, stellen Sie einen angemessenen PooledConnectionLifetime ein — normalerweise 30-90 Sekunden, damit die Verbindungen nicht schneller ablaufen als die IP auf der Seite des Proxy-Anbieters. Für Aufgaben wie das Parsen von Avito oder das Sammeln von Daten aus der Instagram API ist es bequemer, Residential-Proxys mit Rotation auf der Seite des Anbieters zu verwenden — dann muss die Adresse im Code überhaupt nicht geändert werden, es reicht, einen „Eingangspunkt“ mit automatischem Wechsel der Ausgangs-IP zu halten.

PooledConnectionLifetime und DNS-Rotation

Wenn der Proxy-Anbieter DNS-Rotation verwendet (ein Hostname wird bei jedem Zugriff in verschiedene IPs aufgelöst), ist es entscheidend, einen begrenzten PooledConnectionLifetime festzulegen. Standardmäßig ist er auf Infinite gesetzt, und .NET wird die Verbindung zur alten IP so lange halten, wie es will, und dabei DNS-Updates ignorieren.

var handler = new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromSeconds(30), // Verbindung neu erstellen
    PooledConnectionIdleTimeout = TimeSpan.FromSeconds(15),
    MaxConnectionsPerServer = 20
};

Dies ist besonders wichtig für mobile Proxys, bei denen der Wechsel der IP durch den Anbieter alle 1-10 Minuten im Rahmen der Rotation des Mobilfunkanbieters erfolgen kann. Wenn die Verbindung länger gehalten wird, werden die Anfragen weiterhin über die bereits freigegebene IP gesendet, und manchmal erhalten sie sogar einen Timeout-Fehler. Für solche Szenarien sind mobile Proxys gut geeignet — sie bieten das höchste Vertrauen von Anti-Fraud-Systemen, erfordern jedoch eine sorgfältige Einstellung der TTL-Verbindungen auf der Client-Seite.

Ein kurzes PooledConnectionLifetime ist nicht kostenlos — jede Neuerstellung einer Verbindung bedeutet einen neuen TCP+TLS-Handshake, was 50-200 ms Verzögerung hinzufügt. Das Gleichgewicht zwischen der Frische der IP und der Leistung wird empirisch ermittelt: Für Preisüberwachungsaufgaben sind 60-120 Sekunden ausreichend, für hochfrequente Kontoerstellung manchmal 10-20 Sekunden.

Vergleich der Proxy-Typen für .NET-Anwendungen

Proxy-Typ Protokoll Empfohlene PooledConnectionLifetime Aufgaben
Rechenzentrumsproxys HTTP/HTTPS, SOCKS5 5-10 Minuten Parsing, API-Integrationen, hohe Geschwindigkeit
Residential-Proxys HTTP/HTTPS, SOCKS5 30-90 Sekunden Preisüberwachung, Umgehung von Geo-Blockaden
Mobile Proxys HTTP/HTTPS, SOCKS5 10-30 Sekunden Automatisierung in sozialen Netzwerken, anti-fraud-sensible APIs

IHttpClientFactory und typisierte Clients mit Proxys

In ASP.NET Core-Projekten wird empfohlen, HttpClient über IHttpClientFactory zu registrieren — dies entlastet den Entwickler von der Verantwortung für das Management des Lebenszyklus von Handlern und verwendet automatisch interne SocketsHttpHandler.

builder.Services.AddHttpClient("ProxyClient")
    .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
    {
        Proxy = new WebProxy(new Uri("socks5://proxy-host:1080"))
        {
            Credentials = new NetworkCredential("login", "password")
        },
        UseProxy = true,
        PooledConnectionLifetime = TimeSpan.FromMinutes(2)
    })
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

Beachten Sie SetHandlerLifetime — dies ist ein separater Mechanismus zur Rotation der Handler innerhalb der Fabrik (standardmäßig 2 Minuten), der nicht mit PooledConnectionLifetime einer einzelnen Verbindung verwechselt werden sollte. Die Fabrik erstellt regelmäßig einen neuen Handler, aber die alten Verbindungen leben weiter, bis aktive Anfragen abgeschlossen sind — der Garbage Collector räumt sie selbst auf.

Für Szenarien mit mehreren Proxy-Pools ist es bequem, mehrere benannte Clients zu registrieren — beispielsweise einen separat für die USA und einen separat für Europa, wenn die Aufgabe darin besteht, Google Ads oder Facebook Ads aus verschiedenen Regionen zu testen:

builder.Services.AddHttpClient("US-Proxy")
    .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
    {
        Proxy = new WebProxy("http://us-proxy-host:8000"),
        UseProxy = true
    });

builder.Services.AddHttpClient("EU-Proxy")
    .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
    {
        Proxy = new WebProxy("http://eu-proxy-host:8000"),
        UseProxy = true
    });

Diagnose von Socket-Leckagen

Wenn Sie den Verdacht haben, dass Sockets sich ansammeln, raten Sie nicht — überprüfen Sie es. Unter Linux ist ein schneller Weg, die Anzahl der offenen Verbindungen eines Prozesses zu sehen:

# Anzahl der Verbindungen im TIME_WAIT
ss -tan state time-wait | wc -l

# Alle Verbindungen eines bestimmten Prozesses (PID)
ls -la /proc/{PID}/fd | grep socket | wc -l

Unter Windows liefert der Befehl netstat -ano ähnliche Statistiken, gefiltert nach der PID des Prozesses. Für eine tiefere Diagnose auf .NET-Ebene verwenden Sie das integrierte Tool dotnet-counters, das die Metriken System.Net.Http in Echtzeit anzeigt:

dotnet-counters monitor -p {PID} --counters System.Net.Http

# Wichtige Zähler:
# current-connections
# connections-duration
# requests-failed-rate

Wenn current-connections monoton steigt und nie sinkt — ist das ein direktes Zeichen dafür, dass der Handler zu oft neu erstellt wird oder PooledConnectionIdleTimeout auf Infinite gesetzt ist, und Anfragen nicht korrekt abgeschlossen werden (z. B. wird der Antwortkörper nicht vollständig gelesen, und die Verbindung wird nicht in den Pool zurückgegeben).

Ein weiterer häufiger Grund für Leckagen ist das vergessene response.Content, das nicht gelesen oder nicht disposed wurde. Verwenden Sie immer using var response = ... oder einen expliziten Aufruf von Dispose() nach Abschluss der Arbeit mit der Antwort.

Fazit

Der richtige Umgang mit Proxys in .NET basiert auf drei Prinzipien: Verwenden Sie SocketsHttpHandler anstelle von veralteten Implementierungen, halten Sie HttpClient als Singleton (oder über IHttpClientFactory), und implementieren Sie die Proxy-Rotation über einen benutzerdefinierten IWebProxy, anstatt den gesamten Client neu zu erstellen. Seit .NET 6 wird SOCKS5 nativ unterstützt, was die Notwendigkeit beseitigt, ein eigenes Handshake-Protokoll zu schreiben.

Wenn Ihre Anwendung Marktplätze parst oder Daten über APIs mit hoher Anfrageintensität sammelt, achten Sie auf Rechenzentrumsproxys — sie bieten die maximale Geschwindigkeit bei korrekter Konfiguration des Verbindungspools. Für Aufgaben, bei denen Anonymität und Ähnlichkeit mit normalem Benutzerverkehr wichtig sind, sind Residential-Proxys besser geeignet, und für die Arbeit mit mobilen APIs und sozialen Plattformen — mobile Proxys mit kurzer Lebensdauer der Verbindungen im Code.