.NET é um cavalo de batalha para coleta de dados no backend: I/O assíncrono rápido, tipagem forte e um runtime que lida com alta concorrência com folga. Configurar um proxy residencial no HttpClient leva poucas linhas, mas o C# tem sua própria armadilha, que não tem nada a ver com proxies e tudo a ver com como o HttpClient deve ser usado. Acerte o tempo de vida do client e a parte do proxy fica fácil.
Este é o capítulo de C# da mesma série de proxies residenciais com Python, em Go e em Node.js: o código que funciona, além das armadilhas específicas do .NET.
Tudo abaixo usa o gateway residencial da Shifter: um único endpoint, p.shifter.io:443, com toda a segmentação codificada no nome de usuário. Troque o host e as credenciais para usar outro provedor; a estrutura é a mesma.
O modelo de gateway em um parágrafo
O nome de usuário do proxy carrega sua autenticação e sua segmentação. Você não troca de endpoint para mudar de país ou sessão, você muda a string do nome de usuário:
customer-USERNAME-country-us-sid-abc123-ttl-600country-us direciona para os Estados Unidos, sid fixa uma sessão persistente, ttl mantém aquele IP por N segundos. Omita sid/ttl e cada nova conexão faz a rotação. A senha permanece constante. Em .NET, esse nome de usuário vai para um NetworkCredential, não para a URL do proxy.
A configuração básica: WebProxy e um handler
Você configura o proxy em um handler de mensagens e o passa para o HttpClient. As credenciais vão em um WebProxy via NetworkCredential, não embutidas na 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) // not user:pass@ in the URL};
var handler = new SocketsHttpHandler{ Proxy = proxy, UseProxy = true, PooledConnectionLifetime = TimeSpan.FromMinutes(2) // see gotcha below};
var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) };
var ip = await client.GetStringAsync("https://api.ipify.org");Console.WriteLine(ip); // a US residential IPDuas coisas para internalizar. A string user inclui os flags de segmentação (-country-us), porque é ali que a localização geográfica fica. E as credenciais vão no WebProxy como um NetworkCredential; o HttpClient não vai ler user:pass@host de uma URI de proxy da forma que um navegador poderia.
Prefira SocketsHttpHandler (o handler padrão nas versões modernas do .NET) em vez do mais antigo HttpClientHandler. É a implementação moderna e multiplataforma, e expõe PooledConnectionLifetime, que você precisa pelo motivo explicado abaixo.
Armadilha 1: o problema do tempo de vida do HttpClient
Esta é a armadilha específica do .NET, e ela morde em duas direções opostas.
Crie um new HttpClient() para cada requisição e você vaza sockets: cada client mantém seu próprio pool de conexões, e clients descartados deixam sockets presos em TIME_WAIT. Sob carga, isso esgota as portas disponíveis e sua aplicação começa a lançar SocketException. O conselho conhecido, portanto, é reutilizar um único HttpClient.
Mas um único client mantido para sempre tem o problema oposto: ele armazena em cache resoluções de DNS pelo tempo de vida do pool de conexões e nunca percebe quando o IP de um endpoint muda. A solução não é escolher entre os dois, é usar PooledConnectionLifetime no SocketsHttpHandler, que recicla as conexões do pool em um cronograma para que você mantenha a reutilização de conexões sem fixar um DNS obsoleto. Configure para alguns minutos e reutilize o client.
Reutilizar o client também mantém as conexões aquecidas através do proxy, que é exatamente a sobrecarga que o guia de latência existe para eliminar.
O jeito ASP.NET Core: IHttpClientFactory
Se você está em uma aplicação baseada em DI, não gerencie o HttpClient manualmente. O IHttpClientFactory cuida do tempo de vida do handler e da reciclagem de conexões para você, e permite configurar o proxy uma única vez em um client nomeado.
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 });
// then inject IHttpClientFactory and call factory.CreateClient("shifter")A factory faz a rotação dos handlers subjacentes em seu próprio cronograma, então você obtém pooling e DNS atualizado sem precisar mexer no PooledConnectionLifetime você mesmo. Este é o caminho recomendado para qualquer serviço de longa duração.
Armadilha 2: o proxy fica no handler, então faça a rotação com um client por identidade
Como o proxy e suas credenciais estão embutidos no handler, você não pode variar o nome de usuário do proxy por requisição em um único HttpClient. Uma identidade diferente significa um handler diferente e, portanto, um client diferente. O padrão eficiente é armazenar em cache um client por identidade, para que cada sessão mantenha seu próprio pool de conexões, a mesma ideia de um mapa de agente por identidade em outras 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); });}Dê a cada unidade lógica de trabalho seu próprio sid e faça a rotação entre unidades, não no meio de um fluxo (o post sticky vs rotating cobre essa distinção), e mapeie o trabalho para identidades da forma descrita no post sobre balanceamento de carga. Armazene em cache e reutilize esses clients, não crie um por requisição.
Armadilha 3: limite sua concorrência
O .NET torna trivial disparar milhares de requisições de uma vez com Task.WhenAll, e nada impede que você abra todas as conexões simultaneamente, o que esgota os sockets do seu lado e parece um ataque para o alvo. Limite as requisições em andamento com um SemaphoreSlim:
var gate = new SemaphoreSlim(8); // at most 8 concurrent requests
async Task<string> Fetch(HttpClient client, string url){ await gate.WaitAsync(); try { return await client.GetStringAsync(url); } finally { gate.Release(); }}Limite a concorrência por host de destino, não apenas globalmente, para que um site frágil não seja sobrecarregado enquanto um mais permissivo fica ocioso. Mais paralelismo além da tolerância de um alvo compra bloqueios, não throughput (como evitar bloqueios).
Verifique se você está realmente usando o proxy
Antes de fazer qualquer benchmark ou depurar qualquer outra coisa, confirme o IP de saída:
var json = await client.GetStringAsync("http://ip-api.com/json");Console.WriteLine(json); // expect a residential IP in the targeted countryVer seu próprio IP significa que o handler não está sendo aplicado (geralmente UseProxy deixado como false, ou credenciais no objeto errado). Um travamento indica que a saída local está bloqueada. Ambos os casos são cobertos no guia de diagnóstico de timeout.
Perguntas frequentes
Por que minhas credenciais de proxy não funcionam quando coloco na URL?
O HttpClient não lê user:pass@host de uma URI de proxy. Coloque as credenciais no objeto WebProxy como um NetworkCredential. Como o gateway codifica a segmentação no nome de usuário, esse nome de usuário completo (com os flags -country-...) é o nome de usuário do NetworkCredential, e a senha permanece constante.
HttpClientHandler ou SocketsHttpHandler?
Prefira SocketsHttpHandler nas versões modernas do .NET. É o handler gerenciado padrão, é multiplataforma, e expõe PooledConnectionLifetime, que você precisa para evitar DNS obsoleto em um client de longa duração. HttpClientHandler ainda funciona e delega para ele internamente.
Meu scraper em .NET lança SocketException sob carga. Por quê?
Você quase certamente está criando um new HttpClient() por requisição e esgotando os sockets. Reutilize um único client (ou use IHttpClientFactory), e configure PooledConnectionLifetime para que a reutilização não fixe um DNS obsoleto.
Como faço a rotação de IPs por requisição em C#?
Varie o nome de usuário do proxy, o que significa um handler diferente e, portanto, um client diferente. Armazene em cache um HttpClient por identidade em um ConcurrentDictionary para que cada sessão mantenha seu próprio pool, e escolha o client por unidade de trabalho. Omita o sid no nome de usuário para fazer a rotação a cada nova conexão.
O IHttpClientFactory funciona com proxies?
Sim. Configure o proxy no handler primário via ConfigurePrimaryHttpMessageHandler. A factory gerencia o tempo de vida do handler e a atualização do DNS para você, então é a opção mais limpa em qualquer serviço baseado em DI.
Conclusão
C# com proxies residenciais é rápido de configurar quando você respeita duas coisas: colocar as credenciais em um WebProxy como um NetworkCredential em vez de na URL, e acertar o tempo de vida do HttpClient. Use SocketsHttpHandler com PooledConnectionLifetime, ou IHttpClientFactory em uma aplicação com DI, para reutilizar conexões sem fixar DNS obsoleto e sem vazar sockets. Faça a rotação de geolocalização e sessões armazenando em cache um client por identidade, e limite a concorrência com um SemaphoreSlim.
Acerte isso e o .NET lida com coleta concorrente tão bem quanto qualquer outra tecnologia. Aponte-o para o gateway residencial, e lembre-se de que a qualidade do pool determina o quanto você vai precisar tentar novamente (reputação de IP). A página de preços tem os planos por GB para testar contra seus próprios alvos.