← Bloga geri dön

.NET HttpClient'de Proxy: SocketsHttpHandler, SOCKS5 ve Socket Sızıntısı Olmadan Rotasyon

.NET HttpClient'de SocketsHttpHandler ile proxy'yi doğru şekilde nasıl ayarlayacağımızı, SOCKS5'i nasıl bağlayacağımızı ve socket tükenmesi olmadan IP rotasyonu nasıl organize edeceğimizi inceliyoruz.

📅18 Ekim 2026

Eğer bir zamanlar SocketException: Address already in use hatası aldıysanız veya .NET uygulamanızın birkaç saat proxy ile çalıştıktan sonra aniden mevcut soketlerin tükendiğini gördüyseniz, sorun neredeyse her zaman HttpClient'in yanlış kullanımındadır. Bu yazıda, SocketsHttpHandler üzerinden proxy ayarlamayı, SOCKS5'i bağlamayı ve bağlantı havuzunun bir darboğaza dönüşmemesi için IP döngüsünü nasıl organize edeceğimizi inceleyeceğiz.

Neden HttpClient soketleri "tüketiyor"

Klasik hata, her istek için yeni bir HttpClient örneği oluşturmaktır. Proxy ile çalışırken bu sorun daha da kötüleşir: Her proxy sunucusu üzerinden yapılan bağlantı ayrı bir TCP el sıkışması gerektirir ve eğer proxy'yi anlık olarak değiştiriyorsanız, bağlantı havuzu yeniden kullanılmaz ve soketler TIME_WAIT durumunda takılı kalır.

Windows'ta bu özellikle kritik bir durumdur — varsayılan olarak sınırlı sayıda dinamik port mevcuttur (genellikle 16384) ve eğer uygulama yüksek yoğunlukta isteklerle proxy üzerinden veri işliyorsa, tüm portlar birkaç dakika içinde tükenebilir. Linux'ta durum biraz daha yumuşaktır, ancak bağlantıları yeniden kullanmazsanız sorun devam eder.

Eski HttpClientHandler, .NET Framework döneminden beri mevcut olup, bağlantıların yaşam döngüsü üzerinde esnek kontrol sağlamaz. Bu nedenle, .NET Core 2.1'den itibaren SocketsHttpHandler kullanılması önerilmektedir — tamamen yönetilen kod üzerinde yazılmıştır ve bağlantı havuzuna, DNS zaman aşımına ve proxy ayarlarına doğrudan erişim sağlar.

SocketsHttpHandler: Neden gereklidir

SocketsHttpHandler, .NET Core 2.1 ile birlikte Linux ve macOS'ta varsayılan olarak HttpClient'in inşa edildiği taşıma katmanıdır ve .NET 5 ile birlikte Windows'ta da kullanılmaktadır. Eski WinHttpHandler'dan farklı olarak:

  • PooledConnectionLifetime ayarlamanıza olanak tanır — bu, döngüsel proxy ile çalışırken kritik öneme sahiptir;
  • IWebProxy'ye, programatik olarak anlık olarak geçersiz kılabileceğiniz Proxy delegesi aracılığıyla erişim sağlar;
  • ConnectCallback desteği vardır — bu, SOCKS5 için gerekli olan düşük seviyeli bir kancadır;
  • PooledConnectionIdleTimeout süresi dolduğunda soketleri doğru bir şekilde serbest bırakır, sistemde çöp bırakmaz.

Temel ayar şöyle görünmektedir:

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

Temel kural: Bu handler ile HttpClient bir singleton olmalı veya IHttpClientFactory aracılığıyla oluşturulmalıdır — her istek için yeniden oluşturmayın, aksi takdirde bağlantı havuzunun anlamı kaybolur.

SocketsHttpHandler üzerinden proxy ayarlama

HTTP/HTTPS proxy için, handler seviyesinde Proxy özelliğini ayarlamak yeterlidir. Bu, HTTP CONNECT protokolü üzerinden erişim sağlayan veri merkezi proxy'leri ve konut proxy'leri için çalışır:

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

Farklı hostlar için farklı proxy'ler bağlamak gerekiyorsa (örneğin, bir alan adına kimlik doğrulaması doğrudan gidiyorsa ve scraping proxy üzerinden yapılıyorsa), statik bir nesne yerine bir delegat kullanın:

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

Pazar yerlerini tarama görevleri için — örneğin, Wildberries veya Ozon'da fiyat izleme — genellikle her iki yaklaşım birleştirilir: ana trafik, veri merkezi proxy'leri üzerinden geçerken, captcha hizmetlerine veya kimlik doğrulama istekleri doğrudan yapılır.

.NET'te SOCKS5 desteği

.NET 6'dan itibaren, SocketsHttpHandler, proxy URI'sinde şemayı belirterek SOCKS4/SOCKS4a/SOCKS5 için yerel destek aldı — herhangi bir üçüncü taraf kütüphaneye ihtiyaç yoktur:

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

Önemli bir nokta: .NET 6'dan önce böyle bir imkan yoktu ve geliştiricilerin TLS bağlantısı kurmadan önce SOCKS5 el sıkışmasını manuel olarak gerçekleştiren kendi ConnectCallback'larını yazmaları gerekiyordu. Projeniz hala .NET Core 3.1 veya .NET 5'te ise, bu yol hâlâ geçerlidir:

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

Pratikte, üretim projeleri için .NET 6+ sürümüne güncellenmek ve yerleşik uygulamayı kullanmak daha kolaydır — bu, çalışma zamanında testlerle kapsanmıştır ve el sıkışma protokolünü desteklemeyi gerektirmez.

HttpClient'i yeniden oluşturmadan proxy döngüsü

IP döngüsü uygularken sık yapılan bir hata, her proxy değişiminde yeni bir HttpClient (yani yeni bir handler ve yeni bir bağlantı havuzu) oluşturmaktır. Bu, kapatılmamış soketlerin birikmesine kesin olarak yol açar. Doğru yaklaşım, her çağrıda farklı bir proxy adresi döndüren kendi IWebProxy'nizi uygulamaktır:

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

Bu sınıf, aynı SocketsHttpHandler ile bağlanır:

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

Bu şema, iç bağlantı havuzunu yeniden kullanır, ancak her istek gerçekten yeni bir IP üzerinden gider. Her proxy için paralel bağlantı havuzları oluşturuyorsanız (hız için), makul bir PooledConnectionLifetime ayarlayın — genellikle 30-90 saniye, böylece bağlantılar, proxy sağlayıcısındaki IP'den daha hızlı bir şekilde eski hale gelmez. Avito gibi görevler veya Instagram API'sinden veri toplama gibi görevler için, konut proxy'leri kullanmak daha uygundur; bu durumda kodda adres değiştirmeye gerek kalmaz, sadece otomatik olarak çıkış IP'sini değiştiren bir "giriş noktası" tutmak yeterlidir.

PooledConnectionLifetime ve DNS döngüsü

Proxy sağlayıcısı DNS döngüsü kullanıyorsa (bir host adı her erişimde farklı IP'lere çözülüyorsa), sınırlı bir PooledConnectionLifetime ayarlamak kritik öneme sahiptir. Varsayılan olarak Infinite olarak ayarlanmıştır ve .NET, eski IP'ye olan bağlantıyı istediği kadar uzun tutar, DNS güncellemelerini göz ardı eder.

var handler = new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromSeconds(30), // bağlantıyı yeniden oluştur
    PooledConnectionIdleTimeout = TimeSpan.FromSeconds(15),
    MaxConnectionsPerServer = 20
};

Bu, mobil proxy'ler için özellikle önemlidir; burada sağlayıcı IP'yi her 1-10 dakikada bir değiştirebilir. Bağlantıyı daha uzun süre tutarsanız, istekler zaten serbest bırakılmış bir IP üzerinden devam eder ve bazen zaman aşımı hatası alabilirsiniz. Bu tür senaryolar için mobil proxy'ler iyi bir seçenektir — bunlar, anti-fraud sistemleri tarafından en yüksek güveni sağlar, ancak bağlantı TTL'sini dikkatlice ayarlamayı gerektirir.

Kısa bir PooledConnectionLifetime değeri bedava değildir — her bağlantı yeniden oluşturma, yeni bir TCP+TLS el sıkışması anlamına gelir, bu da 50-200 ms gecikme ekler. IP'nin tazeliği ile performans arasındaki denge deneysel olarak belirlenir: fiyat izleme görevleri için 60-120 saniye yeterlidir, yüksek frekanslı hesap alımları için bazen 10-20 saniye gerekir.

.NET uygulamaları için proxy türlerinin karşılaştırılması

Proxy Türü Protokol Tavsiye Edilen PooledConnectionLifetime Görevler
Veri Merkezi Proxy'leri HTTP/HTTPS, SOCKS5 5-10 dakika Tarama, API entegrasyonları, yüksek hız
Konut Proxy'leri HTTP/HTTPS, SOCKS5 30-90 saniye Fiyat izleme, coğrafi engellerin aşılması
Mobil Proxy'ler HTTP/HTTPS, SOCKS5 10-30 saniye Sosyal medyada otomasyon, anti-fraud duyarlı API'ler

IHttpClientFactory ve proxy ile typed clients

ASP.NET Core projelerinde, HttpClient'i IHttpClientFactory aracılığıyla kaydetmek önerilir — bu, geliştiricinin handler'ların yaşam döngüsünü yönetme sorumluluğunu alır ve dahili SocketsHttpHandler'ı otomatik olarak yeniden kullanır.

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

SetHandlerLifetime'a dikkat edin — bu, fabrika içindeki handler'ların döngüsü için ayrı bir mekanizmadır (varsayılan 2 dakikadır), bu da PooledConnectionLifetime'dan ayrı bir bağlantı için geçerlidir. Fabrika periyodik olarak yeni bir handler oluşturur, ancak eski bağlantılar aktif istekler tamamlanana kadar yaşamaya devam eder — çöp toplayıcı bunları kendiliğinden temizler.

Birden fazla proxy havuzuyla senaryolar için, birkaç adlandırılmış istemci kaydetmek uygundur — örneğin, ABD için ayrı ve Avrupa için ayrı, eğer görev Google Ads veya Facebook Ads'i farklı bölgelerden test etmekse:

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

Soket sızıntılarının teşhisi

Eğer soket birikimi olduğundan şüpheleniyorsanız, tahmin etmeyin — kontrol edin. Linux'ta, bir sürecin açık bağlantı sayısını hızlı bir şekilde görmek için:

# TIME_WAIT durumundaki bağlantı sayısı
ss -tan state time-wait | wc -l

# belirli bir sürecin (PID) tüm bağlantıları
ls -la /proc/{PID}/fd | grep socket | wc -l

Windows'ta benzer istatistikleri netstat -ano komutu ile PID sürecine göre filtreleyerek alabilirsiniz. Daha derin bir teşhis için .NET seviyesinde, dotnet-counters adlı yerleşik aracı kullanın; bu, System.Net.Http metriklerini gerçek zamanlı olarak gösterir:

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

# ana sayaçlar:
# current-connections
# connections-duration
# requests-failed-rate

Eğer current-connections monoton bir şekilde artıyorsa ve asla düşmüyorsa — bu, handler'ın çok sık yeniden oluşturulduğunun veya PooledConnectionIdleTimeout'un Infinite olarak ayarlandığının ve isteklerin düzgün bir şekilde tamamlanmadığının (örneğin, yanıtın gövdesi sonuna kadar okunmuyor ve bağlantı havuzuna geri dönmüyor) doğrudan bir işaretidir.

Bir diğer sık neden ise, okunmamış veya dispose edilmemiş response.Content'dir. Her zaman using var response = ... veya yanıtla işiniz bittiğinde Dispose() çağrısını kullanın.

Sonuç

.NET'te proxy ile doğru çalışma, üç ilkeye dayanır: eski uygulamalar yerine SocketsHttpHandler kullanın, HttpClient'i singleton olarak tutun (veya IHttpClientFactory aracılığıyla) ve proxy döngüsünü, tüm istemciyi yeniden oluşturarak değil, özel bir IWebProxy aracılığıyla uygulayın. .NET 6'dan itibaren SOCKS5 yerel olarak desteklenmektedir, bu da kendi el sıkışma protokolünü yazma gerekliliğini ortadan kaldırır.

Uygulamanız pazar yerlerini tarıyorsa veya yüksek yoğunlukta isteklerle API'den veri topluyorsa, veri merkezi proxy'lerine dikkat edin — bunlar, bağlantı havuzunun doğru ayarlanması durumunda maksimum hız sağlar. Anonimlik ve normal kullanıcı trafiği ile benzerlik önemliyse, konut proxy'leri daha iyi bir seçenek olacaktır; mobil API'ler ve sosyal platformlarla çalışırken ise, mobil proxy'ler ile kodda kısa bağlantı süreleri ile çalışmak daha uygundur.