Jika Anda pernah mengalami SocketException: Address already in use atau melihat aplikasi .NET Anda tiba-tiba kehabisan soket yang tersedia setelah beberapa jam menggunakan proksi, — masalahnya hampir selalu terletak pada penggunaan HttpClient yang tidak benar. Dalam artikel ini, kita akan membahas cara mengatur proksi melalui SocketsHttpHandler, menghubungkan SOCKS5, dan mengatur rotasi IP sehingga kumpulan koneksi tidak menjadi hambatan.
Mengapa HttpClient "memakan" soket
Kesalahan klasik adalah membuat instance baru HttpClient untuk setiap permintaan di dalam using. Saat bekerja dengan proksi, masalah ini semakin parah: setiap koneksi melalui server proksi memerlukan TCP handshake terpisah, dan jika Anda juga mengganti proksi secara dinamis, kumpulan koneksi tidak digunakan kembali, dan soket terjebak dalam keadaan TIME_WAIT.
Di Windows, ini sangat kritis — secara default, jumlah port dinamis yang tersedia terbatas (biasanya 16384), dan jika aplikasi mem-parsing data melalui proksi dengan intensitas permintaan tinggi, semua port dapat habis dalam hitungan menit. Di Linux, situasinya sedikit lebih baik berkat pengaturan kernel, tetapi masalah tetap ada jika koneksi tidak digunakan kembali.
HttpClientHandler yang lama, yang masih tersedia sejak zaman .NET Framework, tidak memberikan kontrol fleksibel atas siklus hidup koneksi. Itulah sebabnya mulai dari .NET Core 2.1, disarankan untuk menggunakan SocketsHttpHandler — ini ditulis sepenuhnya dalam kode yang dikelola dan memberikan akses langsung ke kumpulan koneksi, timeout DNS, dan pengaturan proksi.
SocketsHttpHandler: mengapa itu diperlukan
SocketsHttpHandler adalah lapisan transportasi di mana HttpClient dibangun secara default di Linux dan macOS sejak .NET Core 2.1, dan mulai dari .NET 5 — juga di Windows. Berbeda dengan WinHttpHandler yang lama, ia:
- memungkinkan untuk menetapkan
PooledConnectionLifetime— waktu hidup koneksi dalam kumpulan, yang sangat penting saat bekerja dengan proksi yang berotasi; - memberikan akses ke
IWebProxymelalui delegasiProxy, yang dapat diubah secara programatik pada saat runtime; - mendukung
ConnectCallback— hook tingkat rendah untuk mengganti transportasi, yang diperlukan untuk SOCKS5; - secara benar membebaskan soket setelah
PooledConnectionIdleTimeoutberakhir, tanpa meninggalkan sampah di sistem.
Pengaturan dasar terlihat seperti ini:
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)
};
Aturan utama: HttpClient dengan handler ini harus menjadi singleton atau dibuat melalui IHttpClientFactory — jangan buat ulang untuk setiap permintaan, jika tidak, seluruh tujuan kumpulan koneksi akan hilang.
Mengatur proksi melalui SocketsHttpHandler
Untuk proksi HTTP/HTTPS, cukup atur properti Proxy pada tingkat handler. Ini berfungsi untuk proksi pusat data dan proksi residensial yang memberikan akses melalui protokol 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());
Jika perlu menghubungkan proksi yang berbeda untuk host yang berbeda (misalnya, otentikasi ke satu domain dilakukan secara langsung, sementara pengambilan data — melalui proksi), gunakan delegasi alih-alih objek statis:
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;
});
Untuk tugas mem-parsing marketplace — misalnya, memantau harga di Wildberries atau Ozon — sering kali menggabungkan kedua pendekatan: lalu lintas utama melalui proksi pusat data seperti proksi pusat data, sementara permintaan ke layanan captcha atau otorisasi — langsung.
Dukungan SOCKS5 di .NET
Mulai dari .NET 6, SocketsHttpHandler mendapatkan dukungan asli untuk SOCKS4/SOCKS4a/SOCKS5 melalui penunjukan skema dalam URI proksi — tidak memerlukan pustaka pihak ketiga:
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");
Catatan penting: hingga .NET 6, kemampuan ini tidak ada, dan pengembang harus menulis ConnectCallback mereka sendiri yang secara manual melakukan handshake SOCKS5 sebelum membangun koneksi TLS. Jika proyek Anda masih di .NET Core 3.1 atau .NET 5, jalur ini masih relevan:
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);
}
};
Dalam praktiknya, untuk proyek produksi, lebih mudah untuk memperbarui ke .NET 6+ dan menggunakan implementasi bawaan — ini telah diuji dalam runtime dan tidak memerlukan dukungan protokol handshake sendiri.
Rotasi proksi tanpa membuat ulang HttpClient
Kesalahan umum saat menerapkan rotasi IP adalah membuat HttpClient baru (dan dengan demikian, handler baru, dan kumpulan koneksi baru) setiap kali proksi berubah. Ini dijamin akan menyebabkan akumulasi soket yang tidak ditutup. Pendekatan yang benar adalah mengimplementasikan IWebProxy kustom yang mengembalikan alamat proksi yang berbeda pada setiap panggilan:
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;
}
Kelas ini terhubung ke SocketsHttpHandler yang sama:
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);
Skema ini menggunakan kembali kumpulan soket internal, tetapi setiap permintaan benar-benar menggunakan IP baru. Jika Anda mengumpulkan kumpulan koneksi paralel untuk setiap proksi (untuk kecepatan), atur PooledConnectionLifetime yang masuk akal — biasanya 30-90 detik, agar koneksi tidak kedaluwarsa lebih cepat daripada IP di sisi penyedia proksi. Untuk tugas seperti mem-parsing Avito atau mengumpulkan data dari Instagram API, lebih nyaman menggunakan proksi residensial dengan rotasi di sisi penyedia — maka dalam kode tidak perlu mengubah alamat, cukup menjaga satu "titik masuk" dengan penggantian IP keluar otomatis.
PooledConnectionLifetime dan rotasi DNS
Jika penyedia proksi menggunakan rotasi DNS (satu hostname di-resolve ke IP yang berbeda pada setiap permintaan), sangat penting untuk menetapkan PooledConnectionLifetime yang terbatas. Secara default, nilainya adalah Infinite, dan .NET akan mempertahankan koneksi ke IP lama selama yang diinginkan, mengabaikan pembaruan DNS.
var handler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromSeconds(30), // membuat ulang koneksi
PooledConnectionIdleTimeout = TimeSpan.FromSeconds(15),
MaxConnectionsPerServer = 20
};
Ini sangat penting untuk proksi seluler, di mana perubahan IP oleh penyedia dapat terjadi setiap 1-10 menit dalam rotasi operator seluler. Jika koneksi dipertahankan lebih lama, permintaan akan terus menggunakan IP yang sudah dibebaskan, dan kadang-kadang — bahkan mendapatkan kesalahan timeout. Untuk skenario semacam itu, proksi seluler sangat cocok — mereka memberikan kepercayaan tertinggi dari sistem anti-fraud, tetapi memerlukan pengaturan TTL koneksi yang hati-hati di tingkat klien.
Nilai PooledConnectionLifetime yang pendek tidak gratis — setiap pembuatan ulang koneksi berarti handshake TCP+TLS baru, yang menambah latensi 50-200 ms. Keseimbangan antara kesegaran IP dan kinerja ditentukan secara empiris: untuk tugas pemantauan harga, cukup 60-120 detik, untuk pengumpulan akun frekuensi tinggi — kadang-kadang perlu 10-20 detik.
Perbandingan jenis proksi untuk aplikasi .NET
| Jenis proksi | Protokol | PooledConnectionLifetime yang disarankan | Tugas |
|---|---|---|---|
| Proksi pusat data | HTTP/HTTPS, SOCKS5 | 5-10 menit | Pengambilan data, integrasi API, kecepatan tinggi |
| Proksi residensial | HTTP/HTTPS, SOCKS5 | 30-90 detik | Pemantauan harga, melewati geo-blocking |
| Proksi seluler | HTTP/HTTPS, SOCKS5 | 10-30 detik | Automatisasi di media sosial, API sensitif anti-fraud |
IHttpClientFactory dan klien bertipe dengan proksi
Dalam proyek ASP.NET Core, disarankan untuk mendaftarkan HttpClient melalui IHttpClientFactory — ini menghilangkan tanggung jawab pengembang untuk mengelola siklus hidup handler dan secara otomatis menggunakan kembali SocketsHttpHandler internal.
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));
Perhatikan SetHandlerLifetime — ini adalah mekanisme rotasi handler itu sendiri di dalam pabrik (secara default 2 menit), yang tidak boleh bingung dengan PooledConnectionLifetime dari koneksi individual. Pabrik secara berkala membuat handler baru, tetapi koneksi lama terus berlanjut sampai permintaan aktif selesai — garbage collector akan membersihkannya sendiri.
Untuk skenario dengan beberapa kumpulan proksi, nyaman untuk mendaftarkan beberapa klien bernama — misalnya, terpisah untuk AS dan terpisah untuk Eropa, jika tugasnya adalah menguji iklan Google Ads atau Facebook Ads dari berbagai wilayah:
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
});
Diagnostik kebocoran soket
Jika Anda mencurigai akumulasi soket, jangan berspekulasi — periksa. Di Linux, cara cepat untuk melihat jumlah koneksi terbuka dari proses:
# jumlah koneksi dalam TIME_WAIT
ss -tan state time-wait | wc -l
# semua koneksi dari proses tertentu (PID)
ls -la /proc/{PID}/fd | grep socket | wc -l
Di Windows, statistik serupa diberikan oleh perintah netstat -ano dengan penyaringan berdasarkan PID proses. Untuk diagnostik yang lebih mendalam di tingkat .NET, gunakan alat bawaan dotnet-counters, yang menunjukkan metrik System.Net.Http secara real-time:
dotnet-counters monitor -p {PID} --counters System.Net.Http
# penghitung kunci:
# current-connections
# connections-duration
# requests-failed-rate
Jika current-connections meningkat secara monoton dan tidak pernah menurun — ini adalah tanda langsung bahwa handler dibuat ulang terlalu sering, atau PooledConnectionIdleTimeout diatur ke Infinite, dan permintaan tidak selesai dengan benar (misalnya, tubuh respons tidak dibaca sepenuhnya, dan koneksi tidak kembali ke kumpulan).
Penyebab umum lainnya dari kebocoran adalah response.Content yang terlupakan, yang tidak dibaca atau tidak dibersihkan. Selalu gunakan using var response = ... atau panggilan eksplisit Dispose() setelah selesai bekerja dengan respons.
Kesimpulan
Penggunaan proksi yang benar di .NET didasarkan pada tiga prinsip: gunakan SocketsHttpHandler alih-alih implementasi yang sudah usang, pertahankan HttpClient sebagai singleton (atau melalui IHttpClientFactory), dan implementasikan rotasi proksi melalui IWebProxy kustom, bukan dengan membuat ulang seluruh klien. Mulai dari .NET 6, SOCKS5 didukung secara asli, yang menghilangkan kebutuhan untuk menulis protokol handshake sendiri.
Jika aplikasi Anda mem-parsing marketplace atau mengumpulkan data melalui API dengan intensitas permintaan tinggi, perhatikan proksi pusat data — mereka memberikan kecepatan maksimum dengan pengaturan kumpulan koneksi yang benar. Untuk tugas di mana anonimitas dan kesamaan dengan lalu lintas pengguna biasa penting, proksi residensial lebih cocok, dan untuk bekerja dengan API seluler dan platform sosial — proksi seluler dengan waktu hidup koneksi yang pendek dalam kode.