Wissen

Residential Proxies in C# mit HttpClient verwenden

Proxies in C#: WebProxy mit NetworkCredential, SocketsHttpHandler und PooledConnectionLifetime, IHttpClientFactory, und Clients pro Identität für Geo-Rotation.

Chris Collins

Chris Collins

2. August 2026 · 7 Min. Lesezeit

.NET ist ein Arbeitspferd für Backend-Datenerhebung: schnelles async-I/O, starke Typisierung, und eine Runtime, die hohe Nebenläufigkeit bequem bewältigt. Einen Residential Proxy in HttpClient einzubinden sind ein paar Zeilen, aber C# hat seine eigene Falle, die nichts mit Proxies zu tun hat und alles damit, wie HttpClient verwendet werden soll. Bring die Client-Lebensdauer richtig hin, und der Proxy-Teil ist einfach.

Das ist der C#-Beitrag derselben Serie wie Residential Proxies mit Python, mit Go und mit Node.js: der Code, der funktioniert, plus die .NET-spezifischen Fallstricke.

Alles unten nutzt Shifters Residential Gateway: ein Endpunkt, p.shifter.io:443, mit dem gesamten Targeting im Benutzernamen kodiert. Tausche Host und Zugangsdaten für einen anderen Anbieter aus; die Form bleibt gleich.

Das Gateway-Modell in einem Absatz

Der Proxy-Benutzername trägt deine Authentifizierung und dein Targeting. Du wechselst nicht den Endpunkt, um Land oder Session zu ändern, du änderst die Benutzernamen-Zeichenkette:

customer-USERNAME-country-us-sid-abc123-ttl-600

country-us zielt auf die USA, sid fixiert eine Sticky Session, ttl hält diese IP für N Sekunden. Lässt du sid/ttl weg, rotiert jede neue Verbindung. Das Passwort bleibt konstant. In .NET geht dieser Benutzername in ein NetworkCredential, nicht in die Proxy-URL.

Das Grund-Setup: WebProxy und ein Handler

Du konfigurierst den Proxy auf einem Message-Handler und übergibst ihn an HttpClient. Die Zugangsdaten gehen über NetworkCredential auf ein WebProxy, nicht inline in den URI.

using System.Net;
var user = Environment.GetEnvironmentVariable("SHIFTER_USER") + "-country-us";
var pass = Environment.GetEnvironmentVariable("SHIFTER_PASS");
var proxy = new WebProxy("http://p.shifter.io:443")
{
Credentials = new NetworkCredential(user, pass) // kein user:pass@ in der URL
};
var handler = new SocketsHttpHandler
{
Proxy = proxy,
UseProxy = true,
PooledConnectionLifetime = TimeSpan.FromMinutes(2) // siehe Falle unten
};
var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) };
var ip = await client.GetStringAsync("https://api.ipify.org");
Console.WriteLine(ip); // eine US-Residential-IP

Zwei Dinge zum Verinnerlichen. Die user-Zeichenkette enthält die Targeting-Flags (-country-us), weil die Geo dort lebt. Und die Zugangsdaten gehen als NetworkCredential auf das WebProxy, HttpClient liest user:pass@host nicht aus einem Proxy-URI, wie es ein Browser tun könnte.

Bevorzuge SocketsHttpHandler (den Standard-Handler auf modernem .NET) gegenüber dem älteren HttpClientHandler. Er ist die moderne, plattformübergreifende Implementierung und legt PooledConnectionLifetime offen, das du aus dem unten genannten Grund brauchst.

Falle 1: das HttpClient-Lebensdauer-Problem

Das ist die .NET-spezifische Falle, und sie beißt in zwei entgegengesetzten Richtungen.

Erzeuge einen new HttpClient() für jede Anfrage, und du leakst Sockets: jeder Client hält seinen eigenen Verbindungspool, und entsorgte Clients lassen Sockets in TIME_WAIT hängen. Unter Last erschöpft das die verfügbaren Ports, und deine App fängt an, SocketException zu werfen. Der bekannte Rat ist daher, einen einzigen HttpClient wiederzuverwenden.

Aber ein einziger, für immer gehaltener Client hat das entgegengesetzte Problem: er cacht DNS-Auflösungen für die Lebensdauer des Verbindungspools und bemerkt nie, wenn sich die IP eines Endpunkts ändert. Die Lösung ist nicht, zwischen beiden zu wählen, sie ist PooledConnectionLifetime auf SocketsHttpHandler, das gepoolte Verbindungen nach einem Zeitplan recycelt, sodass du Verbindungswiederverwendung behältst, ohne veraltetes DNS festzunageln. Setze es auf ein paar Minuten und verwende den Client wieder.

Den Client wiederzuverwenden hält auch Verbindungen durch den Proxy warm, was genau der Overhead ist, den der Latenz-Leitfaden beseitigen soll.

Der ASP.NET-Core-Weg: IHttpClientFactory

Bist du in einer DI-basierten App, verwalte HttpClient gar nicht von Hand. IHttpClientFactory handhabt die Handler-Lebensdauer und das Verbindungs-Recycling für dich und lässt dich den Proxy einmal auf einem benannten Client konfigurieren.

services.AddHttpClient("shifter")
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
Proxy = new WebProxy("http://p.shifter.io:443")
{
Credentials = new NetworkCredential(
Environment.GetEnvironmentVariable("SHIFTER_USER") + "-country-us",
Environment.GetEnvironmentVariable("SHIFTER_PASS"))
},
UseProxy = true
});
// dann IHttpClientFactory injizieren und factory.CreateClient("shifter") aufrufen

Die Factory rotiert die zugrunde liegenden Handler nach ihrem eigenen Zeitplan, also bekommst du Pooling und frisches DNS, ohne PooledConnectionLifetime selbst anzufassen. Das ist der empfohlene Weg für jeden langlebigen Dienst.

Falle 2: der Proxy sitzt auf dem Handler, also rotiere mit einem Client pro Identität

Da der Proxy und seine Zugangsdaten in den Handler eingebacken sind, kannst du den Proxy-Benutzernamen nicht pro Anfrage auf einem einzigen HttpClient variieren. Eine andere Identität bedeutet einen anderen Handler, und damit einen anderen Client. Das effiziente Muster ist, einen Client pro Identität zu cachen, damit jede Session ihren eigenen Verbindungspool behält, dieselbe Idee wie eine Agent-pro-Identität-Map in anderen Stacks.

using System.Collections.Concurrent;
static readonly ConcurrentDictionary<string, HttpClient> Clients = new();
static HttpClient ClientFor(string country, string sid)
{
var key = $"{country}:{sid ?? "rotate"}";
return Clients.GetOrAdd(key, _ =>
{
var u = Environment.GetEnvironmentVariable("SHIFTER_USER") + "-country-" + country
+ (sid != null ? $"-sid-{sid}-ttl-600" : "");
var handler = new SocketsHttpHandler
{
Proxy = new WebProxy("http://p.shifter.io:443")
{
Credentials = new NetworkCredential(u, Environment.GetEnvironmentVariable("SHIFTER_PASS"))
},
UseProxy = true,
PooledConnectionLifetime = TimeSpan.FromMinutes(2)
};
return new HttpClient(handler);
});
}

Gib jeder logischen Arbeitseinheit ihre eigene sid und rotiere zwischen Einheiten, nicht mitten im Fluss (Sticky vs. rotierend behandelt die Unterscheidung), und ordne Arbeit Identitäten so zu, wie es der Beitrag zum Lastausgleich beschreibt. Cache und verwende diese Clients wieder, baue nicht einen pro Anfrage.

Falle 3: begrenze deine Nebenläufigkeit

.NET macht es trivial, mit Task.WhenAll Tausende Anfragen auf einmal abzufeuern, und nichts hält dich davon ab, jede Verbindung gleichzeitig zu öffnen, was auf deiner Seite Sockets erschöpft und für das Ziel wie ein Angriff aussieht. Begrenze Anfragen in Flug mit einem SemaphoreSlim:

var gate = new SemaphoreSlim(8); // höchstens 8 gleichzeitige Anfragen
async Task<string> Fetch(HttpClient client, string url)
{
await gate.WaitAsync();
try { return await client.GetStringAsync(url); }
finally { gate.Release(); }
}

Begrenze die Nebenläufigkeit pro Ziel-Host, nicht nur global, damit eine fragile Seite nicht malträtiert wird, während eine großzügige verhungert. Mehr Parallelität jenseits der Toleranz eines Ziels kauft Blockaden, keinen Durchsatz (wie man Blockaden vermeidet).

Prüfe, dass du wirklich über den Proxy läufst

Bevor du irgendetwas anderes benchmarkst oder debuggst, bestätige die Exit-IP:

var json = await client.GetStringAsync("http://ip-api.com/json");
Console.WriteLine(json); // erwarte eine Residential-IP im Zielland

Deine eigene IP bedeutet, dass der Handler nicht angewandt wird (meist UseProxy auf false gelassen, oder Zugangsdaten auf dem falschen Objekt). Ein Hänger bedeutet, dass der lokale Ausgang blockiert ist. Beides wird im Leitfaden zur Timeout-Diagnose behandelt.

FAQ

Warum funktionieren meine Proxy-Zugangsdaten nicht, wenn ich sie in die URL packe? HttpClient liest user:pass@host nicht aus einem Proxy-URI. Packe die Zugangsdaten als NetworkCredential auf das WebProxy-Objekt. Da das Gateway das Targeting im Benutzernamen kodiert, ist dieser vollständige Benutzername (mit den -country-...-Flags) der NetworkCredential-Benutzername, und das Passwort ist konstant.

HttpClientHandler oder SocketsHttpHandler? Bevorzuge SocketsHttpHandler auf modernem .NET. Er ist der Standard-Managed-Handler, er ist plattformübergreifend, und er legt PooledConnectionLifetime offen, das du brauchst, um veraltetes DNS auf einem langlebigen Client zu vermeiden. HttpClientHandler funktioniert weiter und delegiert darunter an ihn.

Mein .NET-Scraper wirft unter Last SocketException. Warum? Du erzeugst fast sicher einen new HttpClient() pro Anfrage und erschöpfst Sockets. Verwende einen einzigen Client wieder (oder nutze IHttpClientFactory), und setze PooledConnectionLifetime, damit die Wiederverwendung kein veraltetes DNS festnagelt.

Wie rotiere ich IPs pro Anfrage in C#? Variiere den Proxy-Benutzernamen, was einen anderen Handler und damit einen anderen Client bedeutet. Cache einen HttpClient pro Identität in einem ConcurrentDictionary, damit jede Session ihren eigenen Pool behält, und wähle den Client pro Arbeitseinheit. Lass die sid im Benutzernamen weg, um bei jeder neuen Verbindung zu rotieren.

Funktioniert IHttpClientFactory mit Proxies? Ja. Konfiguriere den Proxy über ConfigurePrimaryHttpMessageHandler auf dem primären Handler. Die Factory verwaltet die Handler-Lebensdauer und die DNS-Frische für dich, also ist sie die sauberste Option in jedem DI-basierten Dienst.

Das Fazit

C# plus Residential Proxies ist schnell, sobald du zwei Dinge respektierst: packe die Zugangsdaten als NetworkCredential auf ein WebProxy statt in die URL, und bring die HttpClient-Lebensdauer richtig hin. Nutze SocketsHttpHandler mit PooledConnectionLifetime, oder IHttpClientFactory in einer DI-App, sodass du Verbindungen wiederverwendest, ohne veraltetes DNS festzunageln und ohne Sockets zu leaken. Rotiere Geo und Sessions, indem du einen Client pro Identität cachst, und begrenze die Nebenläufigkeit mit einem SemaphoreSlim.

Mach das richtig, und .NET bewältigt nebenläufige Sammlung so gut wie irgendetwas. Richte es auf das Residential Gateway, und denk daran, dass die Pool-Qualität entscheidet, wie oft du überhaupt erneut versuchst (IP-Reputation). Die Preisseite hat die Pro-GB-Tarife, um es gegen deine eigenen Ziele zu testen.

Bereit, loszulegen?

Testen Sie Shifters Residential-Proxys, 205M+ IPs, 195+ Länder, ab $0.75/GB.

Jetzt starten