← Torna al blog

Proxy in .NET HttpClient: SocketsHttpHandler, SOCKS5 e rotazione senza perdita di socket

Analizziamo come configurare correttamente un proxy in .NET HttpClient con SocketsHttpHandler, collegare SOCKS5 e organizzare la rotazione degli IP senza esaurimento dei socket.

📅18 ottobre 2026

Se hai mai ricevuto un SocketException: Address already in use o hai visto un'applicazione .NET esaurire improvvisamente i socket disponibili dopo alcune ore di lavoro con un proxy, il problema è quasi sempre dovuto a un uso scorretto di HttpClient. In questo articolo vedremo come configurare un proxy tramite SocketsHttpHandler, collegare SOCKS5 e organizzare la rotazione IP in modo che il pool di connessioni non diventi un collo di bottiglia.

Perché HttpClient "consuma" socket

Un errore classico è creare una nuova istanza di HttpClient per ogni richiesta all'interno di using. Quando si lavora con un proxy, questo problema si aggrava: ogni connessione tramite un server proxy richiede un handshake TCP separato, e se cambi anche il proxy al volo, il pool di connessioni non viene riutilizzato e i socket rimangono in stato TIME_WAIT.

Su Windows questo è particolarmente critico: per impostazione predefinita è disponibile un numero limitato di porte dinamiche (di solito 16384), e se l'applicazione esegue il parsing dei dati tramite proxy con un'alta intensità di richieste, tutte le porte possono esaurirsi in pochi minuti. Su Linux la situazione è un po' più morbida grazie alle impostazioni del kernel, ma il problema persiste se non si riutilizzano le connessioni.

Il vecchio HttpClientHandler, disponibile fin dai tempi del .NET Framework, non offre un controllo flessibile sul ciclo di vita delle connessioni. È per questo che a partire da .NET Core 2.1 si raccomanda di utilizzare SocketsHttpHandler: è scritto interamente in codice gestito e fornisce accesso diretto al pool di connessioni, ai timeout DNS e alle impostazioni del proxy.

SocketsHttpHandler: a cosa serve

SocketsHttpHandler è il livello di trasporto su cui, a partire da .NET Core 2.1, si basa HttpClient per impostazione predefinita su Linux e macOS, e a partire da .NET 5 anche su Windows. A differenza del vecchio WinHttpHandler, esso:

  • permette di impostare PooledConnectionLifetime — la durata della connessione nel pool, che è critica quando si lavora con proxy rotanti;
  • fornisce accesso a IWebProxy tramite il delegato Proxy, che può essere sovrascritto programmaticamente al volo;
  • supporta ConnectCallback — un hook a basso livello per sostituire il trasporto, necessario per SOCKS5;
  • libera correttamente i socket al termine di PooledConnectionIdleTimeout, senza lasciare spazzatura nel sistema.

La configurazione di base appare così:

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)
};

La regola principale è: HttpClient con questo handler deve essere un singleton o creato tramite IHttpClientFactory — non ricrearlo per ogni richiesta, altrimenti tutto il senso del pool di connessioni viene perso.

Configurazione del proxy tramite SocketsHttpHandler

Per i proxy HTTP/HTTPS è sufficiente impostare la proprietà Proxy a livello di handler. Questo funziona per i proxy dei data center e i proxy residenziali che forniscono accesso tramite il protocollo 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 è necessario collegare diversi proxy per diversi host (ad esempio, l'autenticazione a un dominio avviene direttamente, mentre lo scraping avviene tramite proxy), utilizza un delegato invece di un oggetto statico:

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;
});

Per i compiti di parsing dei marketplace — ad esempio, monitorare i prezzi su Wildberries o Ozon — spesso si combinano entrambi gli approcci: il traffico principale passa attraverso i proxy dei data center come proxy dei data center, mentre le richieste ai servizi CAPTCHA o di autenticazione avvengono direttamente.

Supporto SOCKS5 in .NET

A partire da .NET 6, SocketsHttpHandler ha ottenuto supporto nativo per SOCKS4/SOCKS4a/SOCKS5 specificando lo schema nell'URI del proxy — non sono necessarie librerie di terze parti:

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");

Un'importante dettaglio: fino a .NET 6 non era possibile, e gli sviluppatori dovevano scrivere il proprio ConnectCallback, che eseguiva manualmente l'handshake SOCKS5 prima di stabilire una connessione TLS. Se il tuo progetto è ancora su .NET Core 3.1 o .NET 5, questo percorso è ancora attuale:

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);
    }
};

Nella pratica, per i progetti di produzione è più semplice aggiornare a .NET 6+ e utilizzare l'implementazione integrata — è coperta da test in runtime e non richiede supporto autonomo per il protocollo di handshake.

Rotazione del proxy senza ricreare HttpClient

Un errore comune nell'implementazione della rotazione IP è la creazione di un nuovo HttpClient (e quindi di un nuovo handler e di un nuovo pool di connessioni) ad ogni cambio di proxy. Questo porta inevitabilmente all'accumulo di socket non chiusi. L'approccio corretto è implementare un proprio IWebProxy, che restituisce un indirizzo proxy diverso ad ogni chiamata:

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;
}

Questo classe si collega sempre allo stesso 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);

Questo schema riutilizza il pool interno di socket, ma ogni richiesta passa effettivamente attraverso un nuovo IP. Se stai creando pool di connessioni parallele per ogni proxy (per velocità), imposta un PooledConnectionLifetime ragionevole — di solito 30-90 secondi, affinché le connessioni non scadano più velocemente dell'IP dal lato del provider proxy. Per compiti come il parsing di Avito o la raccolta di dati dall'API di Instagram, è più comodo utilizzare proxy residenziali con rotazione dal lato del provider — in questo modo non è necessario cambiare l'indirizzo nel codice, basta mantenere un "punto di ingresso" con sostituzione automatica dell'IP di uscita.

PooledConnectionLifetime e rotazione DNS

Se il provider proxy utilizza la rotazione DNS (un hostname si risolve in IP diversi ad ogni richiesta), è fondamentale impostare un PooledConnectionLifetime limitato. Per impostazione predefinita è Infinite, e .NET manterrà la connessione al vecchio IP per tutto il tempo che desidera, ignorando gli aggiornamenti DNS.

var handler = new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromSeconds(30), // ricreiamo la connessione
    PooledConnectionIdleTimeout = TimeSpan.FromSeconds(15),
    MaxConnectionsPerServer = 20
};

Questo è particolarmente importante per i proxy mobili, dove il cambio di IP da parte del provider può avvenire ogni 1-10 minuti nell'ambito della rotazione dell'operatore di telefonia mobile. Se mantieni la connessione più a lungo, le richieste continueranno a passare attraverso un IP già liberato, e a volte — riceveranno addirittura un errore di timeout. Per scenari simili, sono adatti proxy mobili — offrono la massima fiducia da parte dei sistemi antifrode, ma richiedono una configurazione attenta del TTL delle connessioni proprio a livello client.

Un valore breve di PooledConnectionLifetime non è gratuito: ogni ricreazione della connessione significa un nuovo handshake TCP+TLS, il che aggiunge un ritardo di 50-200 ms. L'equilibrio tra freschezza dell'IP e prestazioni viene trovato empiricamente: per compiti di monitoraggio dei prezzi è sufficiente 60-120 secondi, per il farming ad alta frequenza degli account — a volte sono necessari 10-20 secondi.

Confronto dei tipi di proxy per applicazioni .NET

Tipo di proxy Protocollo PooledConnectionLifetime consigliato Compiti
Proxy dei data center HTTP/HTTPS, SOCKS5 5-10 minuti Parsing, integrazioni API, alta velocità
Proxy residenziali HTTP/HTTPS, SOCKS5 30-90 secondi Monitoraggio dei prezzi, bypass delle geo-blocchi
Proxy mobili HTTP/HTTPS, SOCKS5 10-30 secondi Automazione nei social media, API sensibili all'antifrode

IHttpClientFactory e client tipizzati con proxy

Nei progetti ASP.NET Core, si raccomanda di registrare HttpClient tramite IHttpClientFactory — questo solleva lo sviluppatore dalla responsabilità di gestire il ciclo di vita degli handler e riutilizza automaticamente gli SocketsHttpHandler interni.

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));

Fai attenzione a SetHandlerLifetime — questo è un meccanismo separato per la rotazione degli handler stessi all'interno della fabbrica (di default 2 minuti), che non va confuso con PooledConnectionLifetime di una singola connessione. La fabbrica crea periodicamente un nuovo handler, ma le vecchie connessioni continuano a vivere fino al termine delle richieste attive — il garbage collector le pulirà da solo.

Per scenari con più pool di proxy, è comodo registrare più client nominati — ad esempio, separatamente per gli Stati Uniti e separatamente per l'Europa, se l'obiettivo è testare la pubblicità di Google Ads o Facebook Ads da diverse regioni:

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
    });

Diagnostica delle perdite di socket

Se sospetti un accumulo di socket, non indovinare — controlla. Su Linux, un modo veloce per vedere il numero di connessioni aperte da un processo:

# numero di connessioni in TIME_WAIT
ss -tan state time-wait | wc -l

# tutte le connessioni di un processo specifico (PID)
ls -la /proc/{PID}/fd | grep socket | wc -l

Su Windows, statistiche simili sono fornite dal comando netstat -ano filtrando per PID del processo. Per una diagnosi più approfondita a livello .NET, utilizza lo strumento integrato dotnet-counters, che mostra metriche System.Net.Http in tempo reale:

dotnet-counters monitor -p {PID} --counters System.Net.Http

# contatori chiave:
# current-connections
# connections-duration
# requests-failed-rate

Se current-connections cresce in modo monotono e non diminuisce mai — questo è un chiaro segno che l'handler viene ricreato troppo spesso, oppure PooledConnectionIdleTimeout è impostato su Infinite, e le richieste smettono di completarsi correttamente (ad esempio, il corpo della risposta non viene letto fino alla fine, e la connessione non torna nel pool).

Un'altra causa comune di perdite è response.Content dimenticato, che non è stato letto o non è stato dismesso. Usa sempre using var response = ... o chiama esplicitamente Dispose() dopo aver terminato il lavoro con la risposta.

Conclusione

Un corretto utilizzo dei proxy in .NET si basa su tre principi: utilizza SocketsHttpHandler invece delle implementazioni obsolete, mantieni HttpClient come singleton (o tramite IHttpClientFactory), e implementa la rotazione del proxy tramite un IWebProxy personalizzato, anziché ricreare l'intero client. A partire da .NET 6, SOCKS5 è supportato nativamente, eliminando la necessità di scrivere un protocollo di handshake personalizzato.

Se la tua applicazione esegue il parsing di marketplace o raccoglie dati tramite API con un'alta intensità di richieste, presta attenzione ai proxy dei data center — offrono la massima velocità con una corretta configurazione del pool di connessioni. Per compiti in cui l'anonimato e la somiglianza con il traffico utente normale sono importanti, i proxy residenziali sono più adatti, mentre per lavorare con API mobili e piattaforme sociali, i proxy mobili con un breve tempo di vita delle connessioni nel codice sono la scelta migliore.