← Retour au blog

Proxy dans .NET HttpClient : SocketsHttpHandler, SOCKS5 et rotation sans fuite de sockets

Nous expliquons comment configurer correctement un proxy dans .NET HttpClient avec SocketsHttpHandler, connecter SOCKS5 et organiser la rotation des IP sans épuisement des sockets.

📅18 octobre 2026

Si vous avez déjà rencontré SocketException: Address already in use ou vu votre application .NET manquer soudainement de sockets disponibles après quelques heures d'utilisation avec un proxy, le problème est presque toujours dû à une mauvaise utilisation de HttpClient. Dans cet article, nous allons examiner comment configurer un proxy via SocketsHttpHandler, connecter SOCKS5 et organiser la rotation IP de manière à ce que le pool de connexions ne devienne pas un goulet d'étranglement.

Pourquoi HttpClient "consomme" des sockets

Une erreur classique consiste à créer une nouvelle instance de HttpClient pour chaque requête à l'intérieur d'un using. Lors de l'utilisation de proxies, ce problème s'aggrave : chaque connexion via un serveur proxy nécessite une poignée de main TCP distincte, et si vous changez également de proxy à la volée, le pool de connexions n'est pas réutilisé, et les sockets restent dans l'état TIME_WAIT.

Sur Windows, cela est particulièrement critique : par défaut, un nombre limité de ports dynamiques est disponible (généralement 16384), et si l'application analyse des données via un proxy avec une forte intensité de requêtes, tous les ports peuvent être épuisés en quelques minutes. Sur Linux, la situation est un peu plus douce grâce aux paramètres du noyau, mais le problème persiste si les connexions ne sont pas réutilisées.

L'ancien HttpClientHandler, disponible depuis les temps du .NET Framework, ne permet pas un contrôle flexible sur le cycle de vie des connexions. C'est pourquoi, à partir de .NET Core 2.1, il est recommandé d'utiliser SocketsHttpHandler — il est entièrement écrit en code géré et donne un accès direct au pool de connexions, aux délais d'attente DNS et aux paramètres de proxy.

SocketsHttpHandler : pourquoi en a-t-on besoin

SocketsHttpHandler est le niveau de transport sur lequel HttpClient est construit par défaut sur Linux et macOS depuis .NET Core 2.1, et depuis .NET 5 — aussi sur Windows. Contrairement à l'ancien WinHttpHandler, il :

  • permet de définir PooledConnectionLifetime — la durée de vie de la connexion dans le pool, ce qui est critique lors de l'utilisation de proxies rotatifs ;
  • donne accès à IWebProxy via le délégué Proxy, qui peut être redéfini par programme à la volée ;
  • supporte ConnectCallback — un hook bas niveau pour remplacer le transport, ce qui est nécessaire pour SOCKS5 ;
  • libère correctement les sockets après l'expiration de PooledConnectionIdleTimeout, sans laisser de déchets dans le système.

La configuration de base ressemble à ceci :

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 règle principale : HttpClient avec ce handler doit être un singleton ou créé via IHttpClientFactory — ne le recréez pas pour chaque requête, sinon tout le sens du pool de connexions est perdu.

Configuration du proxy via SocketsHttpHandler

Pour un proxy HTTP/HTTPS, il suffit de définir la propriété Proxy au niveau du handler. Cela fonctionne pour les proxies de centres de données et les proxies résidentiels qui fournissent un accès via le protocole 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());

Si vous devez connecter différents proxies pour différents hôtes (par exemple, l'authentification à un domaine se fait directement, tandis que le scraping se fait via un proxy), utilisez un délégué au lieu d'un objet statique :

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

Pour les tâches de parsing de marketplaces — par exemple, le suivi des prix sur Wildberries ou Ozon — il est courant de combiner les deux approches : le trafic principal passe par des proxies de centres de données comme des proxies de centres de données, tandis que les requêtes vers les services de captcha ou d'authentification se font directement.

Support SOCKS5 dans .NET

À partir de .NET 6, SocketsHttpHandler a obtenu un support natif pour SOCKS4/SOCKS4a/SOCKS5 en spécifiant le schéma dans l'URI du proxy — aucune bibliothèque tierce n'est requise :

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 point important : avant .NET 6, cette possibilité n'existait pas, et les développeurs devaient écrire leur propre ConnectCallback, qui effectuait manuellement la poignée de main SOCKS5 avant d'établir la connexion TLS. Si votre projet est encore sur .NET Core 3.1 ou .NET 5, ce chemin est toujours d'actualité :

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

En pratique, pour les projets de production, il est plus simple de mettre à jour vers .NET 6+ et d'utiliser l'implémentation intégrée — elle est couverte par des tests en temps réel et ne nécessite pas de support autonome pour le protocole de poignée de main.

Rotation des proxies sans recréer HttpClient

Une erreur fréquente lors de la mise en œuvre de la rotation IP est de créer un nouveau HttpClient (et donc un nouveau handler, et un nouveau pool de connexions) à chaque changement de proxy. Cela conduit inévitablement à l'accumulation de sockets non fermés. La bonne approche consiste à implémenter votre propre IWebProxy, qui retourne une adresse proxy différente à chaque appel :

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

Cette classe se connecte toujours au même 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);

Ce schéma réutilise le pool interne de sockets, mais chaque requête passe réellement par une nouvelle IP. Si vous construisez des pools de connexions parallèles pour chaque proxy (pour la vitesse), définissez un PooledConnectionLifetime raisonnable — généralement 30-90 secondes, pour que les connexions ne deviennent pas obsolètes plus rapidement que l'IP du côté du fournisseur de proxy. Pour des tâches comme le parsing d'Avito ou la collecte de données via l'API Instagram, il est plus pratique d'utiliser des proxies résidentiels avec rotation du côté du fournisseur — alors dans le code, il n'est même pas nécessaire de changer l'adresse, il suffit de garder un "point d'entrée" avec un remplacement automatique de l'IP sortante.

PooledConnectionLifetime et rotation DNS

Si le fournisseur de proxy utilise la rotation DNS (un nom d'hôte se résolvant en différentes IP à chaque requête), il est critique de définir un PooledConnectionLifetime limité. Par défaut, il est égal à Infinite, et .NET maintiendra la connexion à l'ancienne IP aussi longtemps qu'il le faudra, ignorant les mises à jour DNS.

var handler = new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromSeconds(30), // recréer la connexion
    PooledConnectionIdleTimeout = TimeSpan.FromSeconds(15),
    MaxConnectionsPerServer = 20
};

Cela est particulièrement important pour les proxies mobiles, où le changement d'IP par le fournisseur peut se produire toutes les 1 à 10 minutes dans le cadre de la rotation de l'opérateur de téléphonie mobile. Si vous maintenez la connexion plus longtemps, les requêtes continueront à passer par une IP déjà libérée, et parfois — même à recevoir une erreur de délai d'attente. Pour de tels scénarios, les proxies mobiles conviennent bien — ils offrent la plus grande confiance de la part des systèmes anti-fraude, mais nécessitent un réglage minutieux des TTL des connexions au niveau du client.

Une valeur courte de PooledConnectionLifetime n'est pas gratuite — chaque recréation de connexion signifie une nouvelle poignée de main TCP+TLS, ce qui ajoute 50-200 ms de latence. L'équilibre entre la fraîcheur de l'IP et la performance est déterminé empiriquement : pour les tâches de suivi des prix, 60-120 secondes suffisent, pour le farming de comptes à haute fréquence — parfois 10-20 secondes sont nécessaires.

Comparaison des types de proxies pour les applications .NET

Type de proxy Protocole PooledConnectionLifetime recommandé Tâches
Proxies de centres de données HTTP/HTTPS, SOCKS5 5-10 minutes Parsing, intégrations API, haute vitesse
Proxies résidentiels HTTP/HTTPS, SOCKS5 30-90 secondes Suivi des prix, contournement des géo-restrictions
Proxies mobiles HTTP/HTTPS, SOCKS5 10-30 secondes Automatisation sur les réseaux sociaux, API sensibles à la fraude

IHttpClientFactory et clients typés avec proxy

Dans les projets ASP.NET Core, il est recommandé d'enregistrer HttpClient via IHttpClientFactory — cela décharge le développeur de la responsabilité de gérer le cycle de vie des handlers et réutilise automatiquement les SocketsHttpHandler internes.

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

Notez le SetHandlerLifetime — c'est un mécanisme distinct de rotation des handlers eux-mêmes au sein de la fabrique (par défaut 2 minutes), qu'il ne faut pas confondre avec PooledConnectionLifetime d'une connexion distincte. La fabrique crée périodiquement un nouveau handler, mais les anciennes connexions continuent de vivre leur temps, tant que les requêtes actives ne sont pas terminées — le garbage collector les nettoiera lui-même.

Pour des scénarios avec plusieurs pools de proxies, il est pratique d'enregistrer plusieurs clients nommés — par exemple, un pour les États-Unis et un pour l'Europe, si la tâche consiste à tester des publicités Google Ads ou Facebook Ads depuis différentes régions :

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

Diagnostic des fuites de sockets

Si vous soupçonnez une accumulation de sockets, ne devinez pas — vérifiez. Sur Linux, un moyen rapide de voir le nombre de connexions ouvertes par le processus :

# nombre de connexions en TIME_WAIT
ss -tan state time-wait | wc -l

# toutes les connexions d'un processus spécifique (PID)
ls -la /proc/{PID}/fd | grep socket | wc -l

Sur Windows, une statistique similaire est fournie par la commande netstat -ano en filtrant par PID du processus. Pour un diagnostic plus approfondi au niveau de .NET, utilisez l'outil intégré dotnet-counters, qui montre les métriques System.Net.Http en temps réel :

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

# compteurs clés :
# current-connections
# connections-duration
# requests-failed-rate

Si current-connections augmente de manière monotone et ne diminue jamais — c'est un signe direct que le handler est recréé trop souvent, ou que PooledConnectionIdleTimeout est défini sur Infinite, et que les requêtes ne se terminent pas correctement (par exemple, le corps de la réponse n'est pas lu jusqu'à la fin, et la connexion ne revient pas dans le pool).

Une autre cause fréquente de fuites est un response.Content oublié, qui n'a pas été lu ou n'a pas été disposé. Utilisez toujours using var response = ... ou un appel explicite à Dispose() après avoir terminé le travail avec la réponse.

Conclusion

Un bon fonctionnement avec les proxies dans .NET repose sur trois principes : utilisez SocketsHttpHandler au lieu des implémentations obsolètes, gardez HttpClient comme singleton (ou via IHttpClientFactory), et implémentez la rotation des proxies via un IWebProxy personnalisé, et non par la recréation de l'ensemble du client. À partir de .NET 6, SOCKS5 est pris en charge nativement, ce qui élimine le besoin d'écrire un protocole de poignée de main personnalisé.

Si votre application analyse des marketplaces ou collecte des données via des API avec une forte intensité de requêtes, portez une attention particulière aux proxies de centres de données — elles offrent la vitesse maximale avec une configuration correcte du pool de connexions. Pour des tâches où l'anonymat et la similarité avec le trafic utilisateur normal sont importants, les proxies résidentiels sont plus adaptés, et pour travailler avec des API mobiles et des plateformes sociales — les proxies mobiles avec un court temps de vie des connexions dans le code.