Używanie proxy HTTP w C# to podstawa web scrapingu, testowania API i automatyzacji — ale .NET ma kilka warstw abstrakcji, które łatwo skonfigurować błędnie. HttpClient, HttpClientHandler, SocketsHttpHandler, WebProxy, IWebProxy — która klasica za co odpowiada? Jak rotować IP bez wycieku połączeń? Jak obsłużyć błędy z Polly i skalować z Parallel.ForEachAsync?
Ten artykuł to przewodnik kod-first dla .NET 8+. Pokażę sześć kompletnych, uruchamialnych przykładów: od podstawowego HttpClient z proxy, przez zaawansowaną konfigurację TLS, aż po produkcyjną pulę rotujących proxy z dependency injection.
1. Podstawowy C# HTTP Proxy z HttpClient i HttpClientHandler
Klasa HttpClientHandler to najprostszy sposób na skonfigurowanie proxy w .NET. Dziedziczy z HttpMessageHandler i udostępnia właściwości takie jak Proxy, UseProxy, Credentials i Proxy.
Pod maską HttpClientHandler deleguje do SocketsHttpHandler w .NET Core 2.1+, ale API pozostaje kompatybilne z klasycznym .NET Framework.
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
public class BasicProxyExample
{
private readonly HttpClient _httpClient;
public BasicProxyExample()
{
// Konfiguracja WebProxy z poświadczeniami
var proxy = new WebProxy("http://gate.proxyhat.com:8080")
{
Credentials = new NetworkCredential("user-country-US", "PASSWORD"),
BypassProxyOnLocal = true
};
// HttpClientHandler z proxy
var handler = new HttpClientHandler
{
Proxy = proxy,
UseProxy = true,
AllowAutoRedirect = true,
MaxAutomaticRedirections = 10,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
};
// HttpClient z czasem życia połączenia
_httpClient = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(30)
};
}
public async Task<string> FetchAsync(string url)
{
try
{
var response = await _httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Błąd HTTP: {ex.Message}");
throw;
}
catch (TaskCanceledException)
{
Console.WriteLine("Timeout — proxy może być wolne lub niedostępne");
throw;
}
}
}
// Użycie
var example = new BasicProxyExample();
var html = await example.FetchAsync("https://httpbin.org/ip");
Console.WriteLine(html);
Kluczowe parametry ProxyHat:
- Host:
gate.proxyhat.com - Port HTTP:
8080 - Port SOCKS5:
1080 - Format URL:
http://USERNAME:PASSWORD@gate.proxyhat.com:8080
Dla C# residential proxies, username zawiera flagi geo-targetingu: user-country-US, user-country-DE-region-state_of_berlin-city-berlin, lub user-sid-abc123 dla sticky sessions.
Bypass List — kiedy pomijać proxy
Właściwość BypassProxyOnLocal pomija proxy dla adresów lokalnych. Dla większej kontroli użyj BypassList:
var proxy = new WebProxy("http://gate.proxyhat.com:8080")
{
Credentials = new NetworkCredential("user-country-US", "PASSWORD"),
BypassProxyOnLocal = true,
BypassList = new[]
{
"localhost",
"127.0.0.1",
"*.internal.company.com",
"10.*",
"192.168.*"
}
};
2. SocketsHttpHandler i PooledConnectionLifetime — zaawansowane połączenia
SocketsHttpHandler to nowoczesny handler wprowadzony w .NET Core 2.1. Daje precyzyjną kontrolę nad pulą połączeń, TLS i timeoutami. W przeciwieństwie do HttpClientHandler, pozwala na per-request proxy przez właściwość Proxy w HttpRequestMessage.
Kluczowa właściwość to PooledConnectionLifetime — czas, po którym połączenie jest usuwane z puli, nawet jeśli jest aktywne. To krytyczne dla C# HTTP proxy z rotacją IP: stare połączenia mogą mieć przypisane IP, które już nie działa.
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
public class SocketsHandlerExample
{
private readonly HttpClient _httpClient;
public SocketsHandlerExample()
{
var handler = new SocketsHttpHandler
{
Proxy = new WebProxy("http://gate.proxyhat.com:8080"),
UseProxy = true,
PooledConnectionLifetime = TimeSpan.FromMinutes(2), // Rotuj połączenia co 2 min
PooledConnectionIdleTimeout = TimeSpan.FromSeconds(30),
MaxConnectionsPerServer = 100,
EnableMultipleHttp2Connections = true,
AutomaticDecompression = DecompressionMethods.All
};
_httpClient = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(60)
};
}
// Per-request proxy — nadpisuje domyślne proxy handlera
public async Task<string> FetchWithCustomProxyAsync(
string url,
string proxyUrl,
string username,
string password)
{
var request = new HttpRequestMessage(HttpMethod.Get, url);
// Ustaw proxy dla tego konkretnego żądania
var proxy = new WebProxy(proxyUrl)
{
Credentials = new NetworkCredential(username, password)
};
// W .NET 8+ można ustawić Proxy na HttpRequestMessage
// ale wymaga to custom message handlera
var handler = new SocketsHttpHandler
{
Proxy = proxy,
UseProxy = true,
PooledConnectionLifetime = TimeSpan.FromMinutes(1)
};
using var client = new HttpClient(handler);
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
public async Task<string> FetchAsync(string url)
{
var response = await _httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
// Przykład z ProxyHat residential proxy
var example = new SocketsHandlerExample();
var result = await example.FetchWithCustomProxyAsync(
"https://httpbin.org/ip",
"http://gate.proxyhat.com:8080",
"user-country-DE-region-state_of_berlin-city-berlin",
"YOUR_PASSWORD"
);
Kiedy używać SocketsHttpHandler zamiast HttpClientHandler?
| Cecha | HttpClientHandler | SocketsHttpHandler |
|---|---|---|
| Kontrola puli połączeń | Ograniczona | Pełna (PooledConnectionLifetime) |
| Per-request proxy | Wymaga custom handlera | Możliwe przez delegaty |
| HTTP/2 | Wspierane | Lepsza kontrola multiple connections |
| Wydajność | Dobra | Najlepsza (bez warstwy HttpClientHandler) |
| Kompatybilność | .NET Framework + Core | .NET Core 2.1+ / .NET 5+ |
3. Polly — Retry i Circuit Breaker dla proxy HTTP
Proxy residential mogą być niestabilne — IP są blokowane, serwery mają przestoje, rate limity odrzucają żądania. Polly to biblioteka .NET do obsługi błędów transitoryjnych z retry, circuit breaker, timeout i bulkhead.
Zainstaluj przez NuGet:
dotnet add package Polly
dotnet add package Polly.Extensions.Http
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Polly;
using Polly.Extensions.Http;
using Polly.Retry;
public class ResilientProxyClient
{
private readonly HttpClient _httpClient;
private readonly AsyncRetryPolicy<HttpResponseMessage> _retryPolicy;
public ResilientProxyClient()
{
var proxy = new WebProxy("http://gate.proxyhat.com:8080")
{
Credentials = new NetworkCredential("user-country-US", "PASSWORD")
};
var handler = new SocketsHttpHandler
{
Proxy = proxy,
UseProxy = true,
PooledConnectionLifetime = TimeSpan.FromMinutes(2)
};
// Policy retry: 3 próby z exponential backoff + jitter
_retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError() // 5xx, 408, network failures
.Or<TaskCanceledException>() // Timeout
.OrResult<HttpResponseMessage>(r =>
r.StatusCode == HttpStatusCode.TooManyRequests || // 429
r.StatusCode == HttpStatusCode.Forbidden) // 403 - może być blokada IP
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)) +
TimeSpan.FromMilliseconds(Random.Shared.Next(0, 1000))); // Jitter
_httpClient = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(30)
};
}
public async Task<string> FetchWithRetryAsync(string url)
{
var response = await _retryPolicy.ExecuteAsync(async () =>
{
var result = await _httpClient.GetAsync(url);
result.EnsureSuccessStatusCode();
return result;
});
return await response.Content.ReadAsStringAsync();
}
// Circuit Breaker — otwiera obwód po 5 kolejnych błędach
public IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30),
onBreak: (outcome, duration) =>
{
Console.WriteLine($"Circuit otwarty na {duration.TotalSeconds}s: {outcome.Exception?.Message}");
},
onReset: () => Console.WriteLine("Circuit zamknięty — działamy"));
}
// Połącz retry + circuit breaker
public async Task<string> FetchResilientAsync(string url)
{
var retryPolicy = _retryPolicy;
var circuitBreaker = GetCircuitBreakerPolicy();
var strategy = Policy.WrapAsync(retryPolicy, circuitBreaker);
var response = await strategy.ExecuteAsync(async () =>
{
var result = await _httpClient.GetAsync(url);
return result;
});
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
var client = new ResilientProxyClient();
var data = await client.FetchResilientAsync("https://example.com/api/data");
4. Parallel.ForEachAsync — równoległy web scraping z proxy
Parallel.ForEachAsync wprowadzony w .NET 6 to nowoczesny sposób na równoległe przetwarzanie z automatycznym zarządzaniem przez ParallelOptions i TaskScheduler. Idealny do scrapingu z .NET HttpClient proxy.
using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Collections.Generic;
public class ParallelScraper
{
private readonly HttpClient _httpClient;
public ParallelScraper()
{
var proxy = new WebProxy("http://gate.proxyhat.com:8080")
{
Credentials = new NetworkCredential("user-country-US", "PASSWORD")
};
var handler = new SocketsHttpHandler
{
Proxy = proxy,
UseProxy = true,
PooledConnectionLifetime = TimeSpan.FromMinutes(2),
MaxConnectionsPerServer = 50 // Limit równoległych połączeń
};
_httpClient = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(30)
};
}
public async Task<Dictionary<string, string>> ScrapeUrlsAsync(IEnumerable<string> urls, int maxConcurrency = 10)
{
var results = new ConcurrentDictionary<string, string>();
var errors = new ConcurrentBag<Exception>();
var options = new ParallelOptions
{
MaxDegreeOfParallelism = maxConcurrency
};
await Parallel.ForEachAsync(urls, options, async (url, cancellationToken) =>
{
try
{
var response = await _httpClient.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync(cancellationToken);
results.TryAdd(url, content);
Console.WriteLine($"[OK] {url} - {content.Length} znaków");
}
catch (Exception ex)
{
errors.Add(ex);
Console.WriteLine($"[BŁĄD] {url} - {ex.Message}");
}
});
if (errors.Count > 0)
{
Console.WriteLine($"Zakończono z {errors.Count} błędami");
}
return new Dictionary<string, string>(results);
}
}
// Użycie
var urls = new List<string>
{
"https://httpbin.org/ip",
"https://httpbin.org/headers",
"https://httpbin.org/user-agent",
"https://example.com",
"https://httpbin.org/get"
};
var scraper = new ParallelScraper();
var results = await scraper.ScrapeUrlsAsync(urls, maxConcurrency: 5);
foreach (var (url, content) in results)
{
Console.WriteLine($"{url}: {content.Length} znaków");
}
Zarządzanie współbieżnością
- MaxDegreeOfParallelism: kontroluje liczbę równoległych zadań
- MaxConnectionsPerServer w SocketsHttpHandler: limit połączeń do jednego hosta
- Rate limiting: szanuj
Retry-Afteri nagłówki rate limit
Dla C# residential proxies, ProxyHat automatycznie rotuje IP, więc możesz zwiększyć współbieżność bez blokad.
5. Rotating Proxy Pool Service z Dependency Injection
W aplikacji produkcyjnej potrzebujesz serwisu, który zarządza pulą proxy, rotuje IP i integruje się z DI kontenerem .NET. Oto kompletna implementacja:
using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
public interface IProxyPool
{
string GetNextProxyUsername();
HttpClient CreateClient(string? proxyUsername = null);
}
public class ProxyHatPool : IProxyPool, IDisposable
{
private readonly ConcurrentQueue<string> _proxyUsernames;
private readonly string _password;
private readonly string _baseUrl;
private readonly ILogger<ProxyHatPool> _logger;
private readonly ConcurrentDictionary<string, HttpClient> _clientCache;
private readonly TimeSpan _connectionLifetime = TimeSpan.FromMinutes(2);
public ProxyHatPool(
string usernamePrefix,
string password,
string[] countries,
ILogger<ProxyHatPool> logger)
{
_password = password;
_baseUrl = "http://gate.proxyhat.com:8080";
_logger = logger;
_clientCache = new ConcurrentDictionary<string, HttpClient>();
// Generuj listę username'ów dla różnych krajów
_proxyUsernames = new ConcurrentQueue<string>();
foreach (var country in countries)
{
for (int i = 0; i < 10; i++)
{
// Format: user-country-US-sid-abc123
var sessionId = Guid.NewGuid().ToString("N")[..8];
var username = $"{usernamePrefix}-country-{country}-sid-{sessionId}";
_proxyUsernames.Enqueue(username);
}
}
_logger.LogInformation("Zainicjalizowano pulę {Count} proxy", _proxyUsernames.Count);
}
public string GetNextProxyUsername()
{
if (_proxyUsernames.TryDequeue(out var username))
{
_proxyUsernames.Enqueue(username); // Rotuj z powrotem
return username;
}
throw new InvalidOperationException("Pula proxy jest pusta");
}
public HttpClient CreateClient(string? proxyUsername = null)
{
proxyUsername ??= GetNextProxyUsername();
// Sprawdź cache — każdy username ma własnego klienta
if (_clientCache.TryGetValue(proxyUsername, out var cachedClient))
{
return cachedClient;
}
var proxy = new WebProxy(_baseUrl)
{
Credentials = new NetworkCredential(proxyUsername, _password)
};
var handler = new SocketsHttpHandler
{
Proxy = proxy,
UseProxy = true,
PooledConnectionLifetime = _connectionLifetime,
PooledConnectionIdleTimeout = TimeSpan.FromSeconds(30),
MaxConnectionsPerServer = 20,
AutomaticDecompression = DecompressionMethods.All
};
var client = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(60)
};
_clientCache.TryAdd(proxyUsername, client);
_logger.LogDebug("Utworzono HttpClient dla proxy {Username}", proxyUsername);
return client;
}
public void Dispose()
{
foreach (var client in _clientCache.Values)
{
client.Dispose();
}
}
}
// Rejestracja w DI
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddProxyPool(
this IServiceCollection services,
string usernamePrefix,
string password,
string[] countries)
{
services.AddSingleton<IProxyPool>(sp =>
{
var logger = sp.GetRequiredService<ILogger<ProxyHatPool>>();
return new ProxyHatPool(usernamePrefix, password, countries, logger);
});
return services;
}
}
// Użycie w Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
var services = new ServiceCollection();
services.AddLogging(builder => builder.AddConsole());
services.AddProxyPool(
usernamePrefix: "user",
password: "YOUR_PASSWORD",
countries: new[] { "US", "DE", "GB", "FR" }
);
var provider = services.BuildServiceProvider();
var proxyPool = provider.GetRequiredService<IProxyPool>();
// Użycie w serwisie aplikacji
for (int i = 0; i < 5; i++)
{
var client = proxyPool.CreateClient();
var ip = await client.GetStringAsync("https://httpbin.org/ip");
Console.WriteLine($"Request {i}: {ip}");
}
Zalety puli proxy z DI
- Rotacja automatyczna: każde żądanie może używać innego IP
- Cache klientów: HttpClient jest ponownie używany dla tego samego username
- Zarządzanie czasem życia: PooledConnectionLifetime zapobiega wyciekom połączeń
- Logowanie: integracja z ILogger dla monitoringu
6. TLS, SslClientAuthenticationOptions i Certificate Pinning
Przy pracy z proxy musisz rozważyć bezpieczeństwo TLS. Czy ufasz certyfikatowi proxy? Czy serwer docelowy akceptuje połączenia przez proxy? Oto jak skonfigurować TLS w .NET 8.
Custom Root CA — zaufanie własnemu certyfikatowi
Niektóre proxy (zwłaszcza datacenter) używają MITM do inspekcji ruchu. Musisz dodać ich CA do zaufanych:
using System;
using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
public class TlsProxyClient
{
private readonly HttpClient _httpClient;
private readonly X509Certificate2 _customRootCa;
public TlsProxyClient(string proxyUrl, string username, string password, string rootCaPath)
{
// Załaduj custom root CA
_customRootCa = new X509Certificate2(rootCaPath);
var proxy = new WebProxy(proxyUrl)
{
Credentials = new NetworkCredential(username, password)
};
var handler = new SocketsHttpHandler
{
Proxy = proxy,
UseProxy = true,
SslOptions = new SslClientAuthenticationOptions
{
EnabledSslProtocols = SslProtocols.Tls13 | SslProtocols.Tls12,
// Custom walidacja certyfikatu
RemoteCertificateValidationCallback = ValidateCertificate,
// Certificate pinning
TargetHost = "your-target-host.com"
},
PooledConnectionLifetime = TimeSpan.FromMinutes(2)
};
_httpClient = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(30)
};
}
private bool ValidateCertificate(
object sender,
X509Certificate2? certificate,
X509Chain? chain,
SslPolicyErrors sslPolicyErrors)
{
// Jeśli nie ma błędów, akceptuj
if (sslPolicyErrors == SslPolicyErrors.None)
return true;
// Jeśli błąd to niezaufany root, sprawdź custom CA
if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateChainErrors) && chain != null)
{
// Dodaj custom CA do chain
chain.ChainPolicy.ExtraStore.Add(_customRootCa);
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
if (chain.Build(certificate!))
{
Console.WriteLine("Certyfikat zweryfikowany przez custom CA");
return true;
}
}
Console.WriteLine($"Błąd walidacji certyfikatu: {sslPolicyErrors}");
return false;
}
// Certificate pinning — tylko konkretny certyfikat
public async Task<string> FetchWithPinningAsync(string url, string expectedCertThumbprint)
{
var handler = new SocketsHttpHandler
{
Proxy = new WebProxy("http://gate.proxyhat.com:8080")
{
Credentials = new NetworkCredential("user-country-US", "PASSWORD")
},
UseProxy = true,
SslOptions = new SslClientAuthenticationOptions
{
RemoteCertificateValidationCallback = (sender, cert, chain, errors) =>
{
if (cert == null) return false;
var thumbprint = cert.GetCertHashString();
var isValid = thumbprint.Equals(expectedCertThumbprint, StringComparison.OrdinalIgnoreCase);
if (!isValid)
{
Console.WriteLine($"Certificate pinning failed! Expected: {expectedCertThumbprint}, Got: {thumbprint}");
}
return isValid;
}
}
};
using var client = new HttpClient(handler);
var response = await client.GetAsync(url);
return await response.Content.ReadAsStringAsync();
}
public async Task<string> FetchAsync(string url)
{
var response = await _httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
// Użycie
var client = new TlsProxyClient(
"http://gate.proxyhat.com:8080",
"user-country-US",
"PASSWORD",
"path/to/root-ca.crt"
);
var data = await client.FetchAsync("https://example.com");
Najlepsze praktyki TLS z proxy
- TLS 1.3: używaj najnowszego protokołu gdy to możliwe
- Certificate pinning: dla krytycznych endpointów, weryfikuj thumbprint certyfikatu
- Custom CA: dodaj do zaufanych tylko jeśli kontrolujesz proxy
- Wyjątki walidacji: loguj wszystkie błędy SSL dla debugowania
Uwaga: Residential proxy jak ProxyHat zazwyczaj nie wykonują MITM — ruch jest szyfrowany end-to-end. Certificate pinning jest potrzebny głównie przy datacenter proxy z inspekcją SSL.
Porównanie podejść do proxy w .NET 8
| Podejście | Złożoność | Elastyczność | Wydajność | Use case |
|---|---|---|---|---|
| HttpClientHandler | Niska | Średnia | Dobra | Proste skrypty, jednorazowe proxy |
| SocketsHttpHandler | Średnia | Wysoka | Najlepsza | Produkcja, pule połączeń |
| Polly + Retry | Średnia | Wysoka | Dobra | Niestabilne proxy, rate limiting |
| Parallel.ForEachAsync | Średnia | Średnia | Wysoka | Masowy scraping |
| DI Proxy Pool | Wysoka | Najwyższa | Najlepsza | Aplikacje enterprise, rotacja IP |
Następne kroki
Przetestuj te przykłady z ProxyHat residential proxies — oferują geo-targeting, sticky sessions i wysoką dostępność. Zobacz ceny ProxyHat i dostępne lokalizacje.
Dla większych projektów scrapingowych, przeczytaj nasz przewodnik najlepszych praktyk web scrapingu i SERP tracking.






