Работа с HTTP-прокси в .NET 8+ требует понимания нескольких уровней абстракции: от базового WebProxy до продвинутых сценариев с ротацией IP и TLS-пиннингом. Это руководство покажет, как правильно настроить HttpClient, интегрировать Polly для обработки сбоев и построить production-ready сервис ротации прокси.
Базовая настройка: HttpClientHandler + WebProxy
Классический способ использования прокси в .NET — через HttpClientHandler с настроенным WebProxy. Этот подход поддерживает аутентификацию, обход локальных адресов и кастомные настройки.
using System.Net;
using System.Net.Http;
// Создаём прокси с учётными данными
var proxy = new WebProxy("http://gate.proxyhat.com:8080", bypassOnLocal: true)
{
Credentials = new NetworkCredential("user-country-US", "PASSWORD"),
BypassList = new[] { "localhost", "127.0.0.1", "*.internal.local" }
};
// Настраиваем HttpClientHandler
var handler = new HttpClientHandler
{
Proxy = proxy,
UseProxy = true,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
AllowAutoRedirect = true,
MaxAutomaticRedirections = 5
};
// HttpClient предназначен для повторного использования
var httpClient = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(30)
};
// Использование
var response = await httpClient.GetAsync("https://httpbin.org/ip");
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Response: {content}");
Важно: HttpClient следует создавать один раз и переиспользовать. Создание нового экземпляра на каждый запрос приводит к исчерпанию сокетов (socket exhaustion).
Когда использовать HttpClientHandler
- Простые сценарии с фиксированным прокси
- Legacy-проекты (.NET Framework)
- Когда не нужна динамическая смена прокси
SocketsHttpHandler: современный подход в .NET 8+
SocketsHttpHandler — более производительная и гибкая альтернатива HttpClientHandler. Ключевое преимущество: возможность настройки прокси на уровне каждого запроса через HttpMessageInvoker или callback-функции.
using System.Net;
using System.Net.Http;
var handler = new SocketsHttpHandler
{
Proxy = new WebProxy("http://gate.proxyhat.com:8080")
{
Credentials = new NetworkCredential("user-country-DE", "PASSWORD")
},
UseProxy = true,
PooledConnectionLifetime = TimeSpan.FromMinutes(5), // Важно для ротации!
PooledConnectionIdleTimeout = TimeSpan.FromSeconds(60),
EnableMultipleHttp2Connections = true,
AutomaticDecompression = DecompressionMethods.All
};
var httpClient = new HttpClient(handler);
// Пример с несколькими запросами
for (int i = 0; i < 10; i++)
{
var response = await httpClient.GetAsync("https://api.ipify.org?format=json");
var json = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Request {i}: {json}");
}
Per-request прокси через ConnectCallback
Для динамической смены прокси на каждый запрос используйте ConnectCallback:
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
// Пул прокси (пример для residential-прокси ProxyHat)
var proxyEndpoints = new[]
{
("gate.proxyhat.com", 8080, "user-country-US"),
("gate.proxyhat.com", 8080, "user-country-GB"),
("gate.proxyhat.com", 8080, "user-country-DE")
};
var random = new Random();
var handler = new SocketsHttpHandler
{
ConnectCallback = async (context, cancellationToken) =>
{
var (host, port, user) = proxyEndpoints[random.Next(proxyEndpoints.Length)];
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
var proxyEndpoint = new DnsEndPoint(host, port);
await socket.ConnectAsync(proxyEndpoint, cancellationToken);
// Отправляем CONNECT для HTTPS
var connectRequest = $"CONNECT {context.DnsEndPoint.Host}:{context.DnsEndPoint.Port} HTTP/1.1\r\n" +
$"Proxy-Authorization: Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"{user}:PASSWORD"))}\r\n" +
"\r\n";
var connectBytes = Encoding.ASCII.GetBytes(connectRequest);
await socket.SendAsync(new ArraySegment<byte>(connectBytes), SocketFlags.None);
// Читаем ответ прокси
var buffer = new byte[1024];
var received = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), SocketFlags.None);
var response = Encoding.ASCII.GetString(buffer, 0, received);
if (!response.Contains("200"))
throw new InvalidOperationException($"Proxy connection failed: {response}");
return new NetworkStream(socket, ownsSocket: true);
},
PooledConnectionLifetime = TimeSpan.FromSeconds(30) // Короткое время жизни для ротации
};
var client = new HttpClient(handler);
Polly: отказоустойчивость для HTTP-запросов
Polly — библиотека для обработки временных сбоев. Комбинация retry и circuit breaker критична для скрапинга через residential-прокси, где отказы — норма.
using Polly;
using Polly.Retry;
using Polly.CircuitBreaker;
using System.Net.Http.Json;
// Стратегия retry с экспоненциальной задержкой
var retryStrategy = new ResiliencePipelineBuilder<HttpResponseMessage>()
.AddRetry(new RetryStrategyOptions<HttpResponseMessage>
{
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<HttpRequestException>()
.HandleResult(r => (int)r.StatusCode >= 500 || r.StatusCode == System.Net.HttpStatusCode.TooManyRequests),
Delay = TimeSpan.FromSeconds(2),
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
OnRetry = args =>
{
Console.WriteLine($"Retry {args.AttemptNumber} after {args.RetryDelay}");
return ValueTask.CompletedTask;
}
})
.Build();
// Circuit breaker для защиты от каскадных сбоев
var circuitBreaker = new ResiliencePipelineBuilder<HttpResponseMessage>()
.AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage>
{
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<HttpRequestException>()
.HandleResult(r => (int)r.StatusCode >= 500),
FailureRatio = 0.5,
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(30),
OnOpened = args =>
{
Console.WriteLine($"Circuit opened due to: {args.Outcome}");
return ValueTask.CompletedTask;
}
})
.Build();
// Комбинированная стратегия
var resiliencePipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
.AddRetry(new RetryStrategyOptions<HttpResponseMessage>
{
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<HttpRequestException>()
.HandleResult(r => (int)r.StatusCode >= 500 || r.StatusCode == System.Net.HttpStatusCode.TooManyRequests),
Delay = TimeSpan.FromSeconds(1),
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential
})
.AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage>
{
FailureRatio = 0.6,
MinimumThroughput = 5,
BreakDuration = TimeSpan.FromMinutes(1)
})
.Build();
// Использование с HttpClient
var handler = new SocketsHttpHandler
{
Proxy = new WebProxy("http://gate.proxyhat.com:8080")
{
Credentials = new NetworkCredential("user-country-US", "PASSWORD")
}
};
var httpClient = new HttpClient(handler);
async Task<T?> ExecuteWithResilience<T>(string url, CancellationToken ct = default)
{
var response = await resiliencePipeline.ExecuteAsync(
async ctx => await httpClient.GetAsync(url, ctx),
ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<T>(cancellationToken: ct);
}
Parallel.ForEachAsync: параллельный скрапинг
.NET 8 предоставляет Parallel.ForEachAsync — удобный API для параллельного выполнения асинхронных операций с ограничением степени параллелизма.
using System.Collections.Concurrent;
using System.Diagnostics;
// Конфигурация параллелизма
var maxDegreeOfParallelism = Environment.ProcessorCount * 2;
var urls = Enumerable.Range(0, 100)
.Select(i => $"https://httpbin.org/delay/{Random.Shared.Next(1, 3)}")
.ToList();
var results = new ConcurrentBag<(int Index, string Result, TimeSpan Duration)>();
var errors = new ConcurrentBag<(int Index, Exception Error)>();
// Создаём HttpClient с прокси
var handler = new SocketsHttpHandler
{
Proxy = new WebProxy("http://gate.proxyhat.com:8080")
{
Credentials = new NetworkCredential("user-country-US", "PASSWORD")
},
PooledConnectionLifetime = TimeSpan.FromMinutes(2),
MaxConnectionsPerServer = maxDegreeOfParallelism
};
using var httpClient = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) };
var sw = Stopwatch.StartNew();
await Parallel.ForEachAsync(
urls.Select((url, index) => (url, index)),
new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism },
async (item, cancellationToken) =>
{
try
{
var requestSw = Stopwatch.StartNew();
var response = await httpClient.GetAsync(item.url, cancellationToken);
var content = await response.Content.ReadAsStringAsync(cancellationToken);
requestSw.Stop();
results.Add((item.index, $"OK: {response.StatusCode}", requestSw.Elapsed));
}
catch (Exception ex)
{
errors.Add((item.index, ex));
}
});
sw.Stop();
Console.WriteLine($"Completed: {results.Count}/{urls.Count} in {sw.Elapsed.TotalSeconds:F2}s");
Console.WriteLine($"Errors: {errors.Count}");
Console.WriteLine($"Avg time per request: {results.Average(r => r.Duration.TotalMilliseconds):F0}ms");
// Выводим ошибки для анализа
foreach (var error in errors.Take(5))
{
Console.WriteLine($"Error at {error.Index}: {error.Error.Message}");
}
DI-сервис ротации прокси для production
Для production-систем нужен централизованный сервис управления прокси с поддержкой ротации, health-check и интеграцией с DI-контейнером .NET.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
// Интерфейс сервиса
public interface IProxyPoolService
{
Task<HttpClient> CreateClientAsync(string? sessionKey = null, CancellationToken ct = default);
void ReportSuccess(string proxyKey);
void ReportFailure(string proxyKey, Exception? error = null);
IReadOnlyList<ProxyStats> GetStats();
}
public record ProxyStats(string Key, int Successes, int Failures, double SuccessRate);
// Конфигурация
public class ProxyPoolOptions
{
public string Gateway { get; set; } = "gate.proxyhat.com";
public int HttpPort { get; set; } = 8080;
public string Username { get; set; } = "";
public string Password { get; set; } = "";
public string[] Countries { get; set; } = ["US", "GB", "DE", "FR"];
public TimeSpan ConnectionLifetime { get; set; } = TimeSpan.FromMinutes(5);
public int MaxConnectionsPerProxy { get; set; } = 10;
public TimeSpan HealthCheckInterval { get; set; } = TimeSpan.FromMinutes(1);
}
// Реализация
public class ProxyPoolService : IProxyPoolService, IDisposable
{
private readonly ProxyPoolOptions _options;
private readonly ILogger<ProxyPoolService> _logger;
private readonly ConcurrentDictionary<string, ProxyEntry> _proxies = new();
private readonly ConcurrentDictionary<string, HttpClient> _clients = new();
private readonly Timer _healthCheckTimer;
private int _roundRobinIndex = 0;
public ProxyPoolService(ProxyPoolOptions options, ILogger<ProxyPoolService> logger)
{
_options = options;
_logger = logger;
// Инициализация прокси для каждой страны
foreach (var country in options.Countries)
{
var key = $"{country}-{Guid.NewGuid():N}"[..8];
_proxies[key] = new ProxyEntry(country, options);
}
_healthCheckTimer = new Timer(PerformHealthCheck, null,
options.HealthCheckInterval, options.HealthCheckInterval);
}
public async Task<HttpClient> CreateClientAsync(string? sessionKey, CancellationToken ct)
{
var proxyKey = sessionKey ?? GetNextProxyKey();
if (_clients.TryGetValue(proxyKey, out var existingClient))
return existingClient;
var entry = _proxies[proxyKey];
var client = CreateHttpClient(entry);
_clients[proxyKey] = client;
return client;
}
private HttpClient CreateHttpClient(ProxyEntry entry)
{
var proxyUrl = $"http://{entry.Username}:{_options.Password}@{_options.Gateway}:{_options.HttpPort}";
var proxy = new WebProxy(proxyUrl);
var handler = new SocketsHttpHandler
{
Proxy = proxy,
UseProxy = true,
PooledConnectionLifetime = _options.ConnectionLifetime,
MaxConnectionsPerServer = _options.MaxConnectionsPerProxy,
AutomaticDecompression = DecompressionMethods.All,
ConnectTimeout = TimeSpan.FromSeconds(30)
};
return new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(60)
};
}
private string GetNextProxyKey()
{
var keys = _proxies.Keys.ToList();
var index = Interlocked.Increment(ref _roundRobinIndex) % keys.Count;
return keys[index];
}
public void ReportSuccess(string proxyKey)
{
if (_proxies.TryGetValue(proxyKey, out var entry))
entry.RecordSuccess();
}
public void ReportFailure(string proxyKey, Exception? error)
{
if (_proxies.TryGetValue(proxyKey, out var entry))
{
entry.RecordFailure();
_logger.LogWarning(error, "Proxy {Key} failed", proxyKey);
}
}
public IReadOnlyList<ProxyStats> GetStats()
=> _proxies.Select(p => new ProxyStats(
p.Key,
p.Value.Successes,
p.Value.Failures,
p.Value.SuccessRate)).ToList();
private void PerformHealthCheck(object? state)
{
foreach (var (key, entry) in _proxies)
{
if (entry.SuccessRate < 0.3 && entry.TotalRequests > 10)
{
_logger.LogWarning("Proxy {Key} has low success rate: {Rate:P}", key, entry.SuccessRate);
}
}
}
public void Dispose()
{
_healthCheckTimer.Dispose();
foreach (var client in _clients.Values)
client.Dispose();
}
private class ProxyEntry
{
public string Country { get; }
public string Username { get; }
private int _successes;
private int _failures;
public ProxyEntry(string country, ProxyPoolOptions options)
{
Country = country;
Username = $"{options.Username}-country-{country}";
}
public int Successes => _successes;
public int Failures => _failures;
public int TotalRequests => _successes + _failures;
public double SuccessRate => TotalRequests > 0 ? (double)_successes / TotalRequests : 1.0;
public void RecordSuccess() => Interlocked.Increment(ref _successes);
public void RecordFailure() => Interlocked.Increment(ref _failures);
}
}
// Регистрация в DI
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddProxyPool(
this IServiceCollection services,
Action<ProxyPoolOptions> configure)
{
services.Configure(configure);
services.AddSingleton<IProxyPoolService, ProxyPoolService>();
return services;
}
}
// Использование в Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProxyPool(options =>
{
options.Username = "your-username";
options.Password = "your-password";
options.Countries = ["US", "GB", "DE", "FR", "JP"];
options.ConnectionLifetime = TimeSpan.FromMinutes(3);
});
var app = builder.Build();
app.MapGet("/scrape", async (IProxyPoolService proxyPool) =>
{
var client = await proxyPool.CreateClientAsync();
try
{
var result = await client.GetStringAsync("https://api.ipify.org?format=json");
proxyPool.ReportSuccess("current-proxy-key");
return Results.Ok(result);
}
catch (Exception ex)
{
proxyPool.ReportFailure("current-proxy-key", ex);
return Results.Problem(ex.Message);
}
});
app.Run();
TLS и сертификаты: SslClientAuthenticationOptions
При работе через прокси важно правильно настроить TLS. Для сценариев с самоподписанными сертификатами или corporate MITM-прокси нужен кастомный валидатор.
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
// Вариант 1: Игнорирование SSL-ошибок (только для разработки!)
var insecureHandler = new SocketsHttpHandler
{
Proxy = new WebProxy("http://gate.proxyhat.com:8080")
{
Credentials = new NetworkCredential("user-country-US", "PASSWORD")
},
SslOptions = new SslClientAuthenticationOptions
{
RemoteCertificateValidationCallback = (sender, cert, chain, errors) =>
{
// ВНИМАНИЕ: Не используйте в production!
return true;
},
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13
}
};
// Вариант 2: Certificate pinning (production-ready)
public class CertificatePinningHandler : DelegatingHandler
{
private readonly HashSet<string> _allowedPublicKeys;
public CertificatePinningHandler(HttpMessageHandler innerHandler, IEnumerable<string> allowedPublicKeys)
: base(innerHandler)
{
_allowedPublicKeys = new HashSet<string>(allowedPublicKeys);
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
// SocketsHttpHandler с пиннингом
var sslOptions = new SslClientAuthenticationOptions
{
RemoteCertificateValidationCallback = (sender, cert, chain, errors) =>
{
if (cert is not X509Certificate2 certificate)
return false;
var publicKey = certificate.GetPublicKeyString();
return _allowedPublicKeys.Contains(publicKey);
},
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13
};
return await base.SendAsync(request, cancellationToken);
}
}
// Вариант 3: Custom Root CA (для corporate-сред)
public static SocketsHttpHandler CreateHandlerWithCustomCa(string caCertPath)
{
var caCertificate = X509Certificate2.CreateFromPemFile(caCertPath);
return new SocketsHttpHandler
{
Proxy = new WebProxy("http://gate.proxyhat.com:8080")
{
Credentials = new NetworkCredential("user-country-US", "PASSWORD")
},
SslOptions = new SslClientAuthenticationOptions
{
RemoteCertificateValidationCallback = (sender, cert, chain, errors) =>
{
if (errors == SslPolicyErrors.None)
return true;
// Проверяем, подписан ли сертификат нашим CA
using var customChain = new X509Chain();
customChain.ChainPolicy.ExtraStore.Add(caCertificate);
customChain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
return customChain.Build((X509Certificate2)cert!);
},
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13
}
};
}
// Пример использования с pinned сертификатами
var knownPublicKeys = new[]
{
"3082010A0282010100...", // SHA-256 public key целевого сервера
"3082010A0282010101..." // Альтернативный ключ
};
var innerHandler = new SocketsHttpHandler
{
Proxy = new WebProxy("http://gate.proxyhat.com:8080")
{
Credentials = new NetworkCredential("user-country-US", "PASSWORD")
}
};
var pinnedHandler = new CertificatePinningHandler(innerHandler, knownPublicKeys);
using var secureClient = new HttpClient(pinnedHandler);
// Теперь запросы будут отклонены, если сертификат сервера не совпадает
var response = await secureClient.GetAsync("https://example.com");
Сравнение подходов к прокси в .NET
| Подход | Производительность | Гибкость | Сложность | Рекомендация |
|---|---|---|---|---|
| HttpClientHandler + WebProxy | Средняя | Низкая | Низкая | Простые сценарии, legacy-код |
| SocketsHttpHandler | Высокая | Высокая | Средняя | Современные .NET 8+ проекты |
| ConnectCallback | Высокая | Очень высокая | Высокая | Per-request ротация прокси |
| DI ProxyPoolService | Высокая | Высокая | Высокая | Enterprise production-системы |
Лучшие практики для C# residential proxies
- Переиспользуйте HttpClient — создавайте один экземпляр на время жизни приложения или через
IHttpClientFactory. - Настройте PooledConnectionLifetime — для ротации IP через residential-прокси устанавливайте 2–5 минут.
- Используйте Polly — retry с exponential backoff и circuit breaker обязательны для production.
- Ограничьте параллелизм —
MaxConnectionsPerServerиMaxDegreeOfParallelismдолжны соответствовать лимитам прокси-провайдера. - Логируйте метрики — успехи/неудачи, latency, IP-адреса для диагностики.
- Уважайте robots.txt и ToS — этичный скрапинг снижает риск блокировок.
Key Takeaway: В .NET 8+
SocketsHttpHandlerс правильно настроеннымPooledConnectionLifetime— лучший выбор для работы с residential-прокси. Комбинируйте с Polly для отказоустойчивости и DI-сервисом для управления пулом.
Заключение
HTTP-прокси в C# и .NET 8 требуют внимания к деталям: правильное управление соединениями, обработка сбоев и TLS-конфигурация. SocketsHttpHandler даёт максимальный контроль, Polly обеспечивает надёжность, а DI-сервис упрощает управление в enterprise-проектах.
Для residential-прокси ProxyHat используйте формат http://user-country-US:PASSWORD@gate.proxyhat.com:8080 и настраивайте PooledConnectionLifetime в соответствии с нужной частотой ротации IP.
Готовы начать? Ознакомьтесь с тарифами ProxyHat и доступными локациями для настройки гео-таргетинга.






