Se você já encontrou SocketException: Address already in use ou viu seu aplicativo .NET de repente ficar sem sockets disponíveis após algumas horas de uso com proxy, o problema quase sempre está no uso inadequado do HttpClient. Neste artigo, vamos discutir como configurar um proxy através do SocketsHttpHandler, conectar SOCKS5 e organizar a rotação de IP para que o pool de conexões não se torne um gargalo.
Por que o HttpClient "consome" sockets
Um erro clássico é criar uma nova instância do HttpClient para cada requisição dentro de um using. Ao trabalhar com proxies, esse problema se agrava: cada conexão através de um servidor proxy requer um handshake TCP separado, e se você ainda muda o proxy em tempo real, o pool de conexões não é reutilizado, e os sockets ficam presos no estado TIME_WAIT.
No Windows, isso é especialmente crítico — por padrão, há um número limitado de portas dinâmicas disponíveis (geralmente 16384), e se o aplicativo está analisando dados através de um proxy com alta intensidade de requisições, todas as portas podem se esgotar em questão de minutos. No Linux, a situação é um pouco mais branda devido às configurações do kernel, mas o problema persiste se as conexões não forem reutilizadas.
O antigo HttpClientHandler, disponível desde os tempos do .NET Framework, não oferece controle flexível sobre o ciclo de vida das conexões. É por isso que, a partir do .NET Core 2.1, é recomendado usar o SocketsHttpHandler — ele é escrito completamente em código gerenciado e oferece acesso direto ao pool de conexões, timeouts de DNS e configurações de proxy.
SocketsHttpHandler: para que serve
O SocketsHttpHandler é o nível de transporte no qual o HttpClient é construído por padrão no Linux e macOS a partir do .NET Core 2.1, e a partir do .NET 5 — também no Windows. Ao contrário do antigo WinHttpHandler, ele:
- permite definir
PooledConnectionLifetime— o tempo de vida da conexão no pool, o que é crítico ao trabalhar com proxies rotativos; - oferece acesso ao
IWebProxyatravés do delegadoProxy, que pode ser sobrescrito programaticamente em tempo real; - suporta
ConnectCallback— um hook de baixo nível para substituir o transporte, necessário para SOCKS5; - libera corretamente os sockets após o
PooledConnectionIdleTimeoutexpirar, sem deixar lixo no sistema.
A configuração básica é assim:
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)
};
A regra principal: HttpClient com este handler deve ser singleton ou criado através do IHttpClientFactory — não o recrie para cada requisição, caso contrário, todo o propósito do pool de conexões se perde.
Configuração de proxy através do SocketsHttpHandler
Para proxies HTTP/HTTPS, é suficiente definir a propriedade Proxy no nível do handler. Isso funciona para proxies de data center e proxies residenciais que fornecem acesso pelo protocolo HTTP CONNECT:
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());
Se você precisa conectar diferentes proxies para diferentes hosts (por exemplo, a autenticação para um domínio vai diretamente, enquanto o scraping — através de um proxy), use um delegado em vez de um objeto estático:
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;
});
Para tarefas de scraping em marketplaces — por exemplo, monitoramento de preços no Wildberries ou Ozon — muitas vezes combina-se ambos os métodos: o tráfego principal passa através de proxies de data center como proxies de data center, enquanto as requisições para serviços de captcha ou autenticação — diretamente.
Suporte a SOCKS5 no .NET
A partir do .NET 6, o SocketsHttpHandler obteve suporte nativo a SOCKS4/SOCKS4a/SOCKS5 através da especificação do esquema na URI do proxy — nenhuma biblioteca externa é necessária:
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");
Um ponto importante: até o .NET 6, essa possibilidade não existia, e os desenvolvedores precisavam escrever seu próprio ConnectCallback, que manualmente realizava o handshake SOCKS5 antes de estabelecer a conexão TLS. Se seu projeto ainda está no .NET Core 3.1 ou .NET 5, esse caminho ainda é relevante:
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);
}
};
Na prática, para projetos de produção, é mais fácil atualizar para .NET 6+ e usar a implementação embutida — ela é coberta por testes em tempo de execução e não requer suporte próprio para o protocolo de handshake.
Rotação de proxy sem recriar HttpClient
Um erro comum ao implementar a rotação de IP é criar um novo HttpClient (e, portanto, um novo handler e um novo pool de conexões) a cada troca de proxy. Isso leva garantidamente ao acúmulo de sockets não fechados. A abordagem correta é implementar seu próprio IWebProxy, que retorna um endereço de proxy diferente a cada chamada:
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;
}
Este classe se conecta ao mesmo único SocketsHttpHandler:
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);
Esse esquema reutiliza o pool interno de sockets, mas cada requisição realmente passa por um novo IP. Se você está criando pools de conexões paralelas para cada proxy (para velocidade), defina um PooledConnectionLifetime razoável — geralmente de 30 a 90 segundos, para que as conexões não expirem mais rápido do que o IP do lado do provedor de proxy. Para tarefas como scraping do Avito ou coleta de dados da API do Instagram, é mais conveniente usar proxies residenciais com rotação do lado do provedor — assim, no código, você não precisa mudar o endereço, basta manter um "ponto de entrada" com substituição automática do IP de saída.
PooledConnectionLifetime e rotação de DNS
Se o provedor de proxy usa rotação de DNS (um hostname resolve em diferentes IPs a cada solicitação), é crítico definir um PooledConnectionLifetime limitado. Por padrão, ele é igual a Infinite, e o .NET manterá a conexão com o antigo IP por tempo indeterminado, ignorando as atualizações de DNS.
var handler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromSeconds(30), // recriando a conexão
PooledConnectionIdleTimeout = TimeSpan.FromSeconds(15),
MaxConnectionsPerServer = 20
};
Isso é especialmente importante para proxies móveis, onde a troca de IP pelo provedor pode ocorrer a cada 1-10 minutos no âmbito da rotação da operadora de telefonia móvel. Se mantiver a conexão por mais tempo, as requisições continuarão a passar pelo IP já liberado, e às vezes — até mesmo receberão um erro de timeout. Para tais cenários, proxies móveis são uma boa opção — eles oferecem a maior confiança por parte dos sistemas antifraude, mas exigem uma configuração cuidadosa do TTL das conexões, exatamente no nível do cliente.
Um valor curto para PooledConnectionLifetime não é gratuito — cada recriação de conexão significa um novo handshake TCP+TLS, o que adiciona 50-200 ms de latência. O equilíbrio entre a frescura do IP e o desempenho deve ser ajustado empiricamente: para tarefas de monitoramento de preços, 60-120 segundos é suficiente, para a agricultura de contas de alta frequência — às vezes, 10-20 segundos são necessários.
Comparação de tipos de proxy para aplicativos .NET
| Tipo de proxy | Protocolo | PooledConnectionLifetime recomendado | Tarefas |
|---|---|---|---|
| Proxies de data center | HTTP/HTTPS, SOCKS5 | 5-10 minutos | Scraping, integrações de API, alta velocidade |
| Proxies residenciais | HTTP/HTTPS, SOCKS5 | 30-90 segundos | Monitoramento de preços, contorno de geo-bloqueios |
| Proxies móveis | HTTP/HTTPS, SOCKS5 | 10-30 segundos | Automação em redes sociais, APIs sensíveis a antifraude |
IHttpClientFactory e clientes tipados com proxy
Em projetos ASP.NET Core, é recomendado registrar o HttpClient através do IHttpClientFactory — isso retira do desenvolvedor a responsabilidade de gerenciar o ciclo de vida dos handlers e reutiliza automaticamente os SocketsHttpHandler internos.
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));
Note o SetHandlerLifetime — este é um mecanismo separado de rotação dos próprios handlers dentro da fábrica (por padrão 2 minutos), que não deve ser confundido com PooledConnectionLifetime de uma conexão separada. A fábrica periodicamente cria um novo handler, mas as conexões antigas continuam a existir até que as requisições ativas sejam concluídas — o coletor de lixo as limpará automaticamente.
Para cenários com múltiplos pools de proxy, é conveniente registrar vários clientes nomeados — por exemplo, um separado para os EUA e outro para a Europa, se a tarefa for testar anúncios do Google Ads ou Facebook Ads de diferentes regiões:
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
});
Diagnóstico de vazamentos de sockets
Se você suspeita de acúmulo de sockets, não adivinhe — verifique. No Linux, um jeito rápido de ver a quantidade de conexões abertas do processo:
# número de conexões em TIME_WAIT
ss -tan state time-wait | wc -l
# todas as conexões de um processo específico (PID)
ls -la /proc/{PID}/fd | grep socket | wc -l
No Windows, uma estatística semelhante é fornecida pelo comando netstat -ano filtrando pelo PID do processo. Para um diagnóstico mais profundo no nível do .NET, use a ferramenta embutida dotnet-counters, que mostra métricas System.Net.Http em tempo real:
dotnet-counters monitor -p {PID} --counters System.Net.Http
# contadores chave:
# current-connections
# connections-duration
# requests-failed-rate
Se current-connections cresce de forma monótona e nunca diminui — isso é um sinal direto de que o handler está sendo recriado com muita frequência, ou PooledConnectionIdleTimeout está definido como Infinite, e as requisições não estão sendo concluídas corretamente (por exemplo, o corpo da resposta não é lido até o fim, e a conexão não retorna ao pool).
Outra causa comum de vazamentos é o response.Content esquecido, que não foi lido ou não foi descartado. Sempre use using var response = ... ou uma chamada explícita para Dispose() após terminar de trabalhar com a resposta.
Conclusão
O trabalho correto com proxies no .NET é baseado em três princípios: use SocketsHttpHandler em vez de implementações obsoletas, mantenha o HttpClient como singleton (ou através do IHttpClientFactory), e implemente a rotação de proxies através de um IWebProxy personalizado, e não através da recriação de todo o cliente. A partir do .NET 6, o SOCKS5 é suportado nativamente, o que elimina a necessidade de escrever seu próprio protocolo de handshake.
Se seu aplicativo está fazendo scraping de marketplaces ou coletando dados através de APIs com alta intensidade de requisições, considere os proxies de data center — eles oferecem a máxima velocidade com a configuração correta do pool de conexões. Para tarefas onde a anonimidade e a semelhança com o tráfego de usuários normais são importantes, os proxies residenciais são mais adequados, e para trabalhar com APIs móveis e plataformas sociais — proxies móveis com um tempo de vida curto para conexões no código.