در معماریهای نوین نرمافزاری، بهویژه در سیستمهای مبتنی بر Microservices و ابرهای توزیعشده، بخش عمدهای از ارتباطات و تبادل دادهها از طریق پروتکل HTTP انجام میشود. در فریمورک .NET، کلاس HttpClient به عنوان ابزار اصلی برای request و response ها شناخته میشود. با این حال، استفاده سنتی و ناشیانه از این کلاس میتواند به بحرانهای جدی در زیرساخت نرمافزار، از جمله اشباع سوکتها (Socket Exhaustion) و عدم بهروزرسانی تغییرات DNS (DNS Stale) منجر شود.
برای برطرف کردن این چالشهای بنیادین، مایکروسافت در نسخه .NET Core 2.1 مکانیزم پیشرفتهای تحت عنوان IHttpClientFactory معرفی کرد. این مقاله به بررسی کالبدشکافی عمیق چرخه حیات سوکتها، نحوه عملکرد الگوی کارخانهای این ابزار، روشهای پیادهسازی و الگوهای پیشرفته پایداری سیستم (Resilience Patterns) میپردازد.
قبل از پرداختن به ساختار IHttpClientFactory، ابتدا باید درک کنیم که چرا استفاده مستقیم از HttpClient در محیطهای سازمانی (Enterprise) یک ضدالگو (Anti-Pattern) محسوب میشود.
مشکل اول: اشباع سوکتها (Socket Exhaustion) و بیانیهی Using
بسیاری از توسعهدهندگان بر اساس یک قاعده کلی در زبان #C آموختهاند که هر کلاسی که اینترفیس IDisposable را پیادهسازی کرده است، باید درون یک بلوک using قرار گیرد تا منابع سیستم بلافاصله پس از اتمام کار آزاد شوند. نمونه کلاسیک این اشتباه به شرح زیر است:
// ضد الگوی رایج در محیطهای عملیاتی
using (var client = new HttpClient())
{
var response = await client.GetAsync("https://api.example.com/data");
// پردازش پاسخ
}
در ظاهر این کد کاملاً درست و تمیز به نظر میرسد، اما در لایههای زیرین سیستمعامل و شبکه، وقتی یک سوکت TCP بسته میشود، به دلایل امنیتی و اطمینان از دریافت تمام بستههای سرگردان در شبکه، به مدت پیشفرض ۲۴۰ ثانیه (۴ دقیقه) در وضعیتی به نام TIME_WAIT باقی میماند. فرمول حداکثر تعداد درخواستهای همزمان قابل پشتیبانی در این مکانیزم به شرح زیر است:
R_{max} = \frac{S_{available}}{T_{WAIT}}
که در آن $S_{available}$ تعداد سوکتهای آزاد سیستمعامل و $T_{WAIT}$ مدت زمان ماندن سوکت در وضعیت انتظار است. اگر نرخ درخواستهای ورودی به اپلیکیشن شما بیشتر از توان تخلیه سوکتها باشد، سرور با خطای کمبود سوکت مواجه شده و کل فرآیند ارتباطی سیستم مختل میگردد (System.Net.Sockets.SocketException).
مشکل دوم: عدم انعطافپذیری در برابر تغییرات DNS با روش Singleton
برای حل مشکل فوق، راهکار اولیه توسعهدهندگان، به اشتراکگذاری یک نمونه static یا ثبت آن به عنوان Singleton در کانتینر DI بود. با این کار، تنها یک سوکت باز میشود و تمامی درخواستها از همان کانال عبور میکنند. اما این روش مشکل دوم را خلق میکند: عدم واکنش به تغییرات DNS.
کلاس HttpClient در هنگام مقداردهی اولیه، آدرس IP مقصد را از طریق سرور DNS بومیازی و کش میکند. اگر سرور مقصد فرآیند Blue-Green Deployment انجام دهد، یا پشت یک Load Balancer قرار داشته باشد که آیپیهای آن تغییر میکنند، این نمونه استاتیک هرگز متوجه تغییر آدرس IP نخواهد شد و درخواستها را به یک مقصد مرده یا قدیمی ارسال میکند.
خلاصه چالش: استفاده لایفتایم Transient باعث نابودی سوکتها (Socket Exhaustion) میشود و لایفتایم Singleton باعث کوری سیستم نسبت به تغییرات فواصل زمانی شبکه و DNS میگردد. راهکار، جداسازی مدیریت نمونه کلاینت از چرخه حیات مدیریت اتصالات زیرین شبکه است.
ابزار IHttpClientFactory دقیقاً به عنوان یک لایه انتزاعی هوشمند وارد عمل میشود تا مدیریت چرخه حیات اتصالات شبکه را به عهده بگیرد. نکته کلیدی اینجاست: این ابزار چرخه حیات خود HttpClient را مدیریت نمیکند، بلکه مدیریت HttpClientHandler (که مسئول ایجاد اتصال شبکه و نگه داشتن سوکت TCP است) را کنترل میکند.
طول عمر پیشفرض برای هر HttpMessageHandler درون کارخانه ۲ دقیقه است. هنگامی که شما متد CreateClient() را فراخوانی میکنید:
یک نمونه جدید و سبک از HttpClient ساخته میشود.
کارخانه یک HttpMessageHandler موجود و فعال را از درون استخر اتصالات خود (Pool) برداشته و به این کلاینت متصل میکند.
پس از اتمام کار و از بین رفتن کلاینت، هاندر به استخر باز میگردد.
وقتی عمر ۲ دقیقهای هاندر به پایان میرسد، کارخانه آن را منقضی علامتگذاری میکند تا درخواستهای جدید از هاندرهای جدید استفاده کنند، اما اتصالات فعلی درون هاندر قدیمی باز میمانند تا پس از پایان پردازش درخواستهای در جریان، به صورت کاملاً امن متلاشی شوند. این کار مانع از بروز اختلال در کش DNS میشود.
داتنت چهار روش یا الگوی مختلف برای استفاده از کارخانه HttpClient ارائه میدهد تا توسعهدهندگان بر اساس نیازهای معماری خود یکی از آنها را انتخاب کنند.
روش اول: استفاده پایه و مستقیم (Basic Usage)
این سادهترین حالت است که نیازی به پیکربندیهای پیچیده ندارد. ابتدا باید ابزار را در بخش تنظیمات سرویسها ثبت کنید:
// Program.cs
builder.Services.AddHttpClient();
سپس در کلاسهای خود از طریق تزریق وابستگی (DI) به آن دسترسی خواهید داشت:
public class OrderService
{
private readonly IHttpClientFactory _clientFactory;
public OrderService(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task GetProductDetailsAsync(int productId)
{
var client = _clientFactory.CreateClient();
var response = await client.GetAsync($"https://api.store.com/products/{productId}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync();
}
}
روش دوم: کلاینتهای نامگذاری شده (Named Clients)
اگر اپلیکیشن شما با چندین API خارجی مختلف در ارتباط است که هر کدام نیاز به تنظیمات اختصاصی (مانند BaseAddress، هدرهای امنیتی یا Timeout) دارند، کلاینتهای نامگذاری شده راهکار مناسبی هستند:
// Program.cs
builder.Services.AddHttpClient("GitHubClient", client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
client.DefaultRequestHeaders.Add("User-Agent", "DotNetCoreApp");
});
استفاده از آن در لایه سرویس:
var client = _clientFactory.CreateClient("GitHubClient");
var response = await client.GetAsync("repos/dotnet/runtime");
روش سوم: کلاینتهای تایپشده (Typed Clients)
بهترین الگو از نظر رعایت اصول شیگرایی، افزایش امنیت در زمان کامپایل (Type Safety) و خوانایی کد، استفاده از کلاینتهای مستقل و تخصصی است. در این الگو، پیکربندی مستقیماً به یک کلاس سرویس مشخص متصل میشود.
// تعریف کلاس سرویس اختصاصی
public class GitHubService
{
private readonly HttpClient _httpClient;
public GitHubService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task GetRepoAsync(string repoName)
{
return await _httpClient.GetFromJsonAsync(`repos/${repoName}`);
}
}
ثبت کلاینت تایپشده در کانتینر DI در فایل Program.cs:
builder.Services.AddHttpClient(client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.Timeout = TimeSpan.FromSeconds(15);
});
یکی از قدرتمندترین ویژگیهای IHttpClientFactory قابلیت ایجاد زنجیرهای از هاندرها (DelegatingHandler) است. این ساختار عملکردی دقیقاً مشابه لایههای Middleware در ASP.NET Core دارد، با این تفاوت که این بار لایهها روی درخواستهای خروجی اعمال میشوند.
شما میتوانید هاندرهای سفارشی برای کارهایی مثل اضافه کردن خودکار توکنهای امنیتی (Bearer Tokens)، ثبت لاگهای اختصاصی شبکه، یا فشردهسازی دادهها ایجاد کنید:
public class LoggingHandler : DelegatingHandler
{
private readonly ILogger _logger;
public LoggingHandler(ILogger logger)
{
_logger = logger;
}
protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
_logger.LogInformation($"ارسال درخواست به آدرس: {request.RequestUri}");
var response = await base.SendAsync(request, cancellationToken);
_logger.LogInformation($"دریافت پاسخ با کد وضعیت: {response.StatusCode}");
return response;
}
}
ثبت و اتصال زنجیره در Program.cs:
builder.Services.AddTransient();
builder.Services.AddHttpClient()
.AddHttpMessageHandler();
در دنیای واقعی، اتصالات شبکه غیرقابل اعتماد هستند؛ قطعیهای لحظهای، تاخیرها و خطاهای موقت (Transient Faults) به وفور رخ میدهند. معماریهای مایکروسرویس باید توانایی ترمیم خودکار (Self-Healing) در برابر این مشکلات را داشته باشند.
در نسخههای جدید داتنت (از .NET 8 به بعد)، قابلیتهای پایداری شبکه با تکیه بر کتابخانه محبوب Polly به صورت بومی و یکپارچه درآمده است. با استفاده از پکیج Microsoft.Extensions.Http.Resilience میتوان سناریوهای پیشرفتهای را به سادگی پیکربندی کرد:
builder.Services.AddHttpClient()
.AddStandardResilienceHandler(options =>
{
// تنظیمات استراتژی تلاش مجدد (Retry)
options.Retry.MaxRetryAttempts = 3;
options.Retry.Delay = TimeSpan.FromSeconds(2);
options.Retry.BackoffType = Polly.DelayBackoffType.Exponential;
// تنظیمات الگوی قطعکننده مدار (Circuit Breaker)
options.CircuitBreaker.FailureRatio = 0.5; // اگر ۵۰ درصد درخواستها در بازه زمانی شکست بخورند
options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);
options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(15); // مدار ۱۵ ثانیه باز میماند
});
معرفی الگوهای پایداری پرکاربرد:
Retry Policy (تلاش مجدد): اگر درخواستی به دلیل خطای موقت (مثلاً کد وضعیت 503) شکست بخورد، سیستم بعد از وقفه کوتاهی مجدداً تلاش میکند. با استفاده از فرمول توانی (Exponential Backoff) زمان انتظار بین هر تلاش افزایش مییابد تا سرور مقصد زیر بار ترافیکی خفه نشود.
Circuit Breaker (قطعکننده مدار): اگر تعداد خطاهای سرور مقصد از حد مجاز فراتر رود، مدار "باز" میشود و دیگر هیچ درخواستی به سمت مقصد فرستاده نمیشود؛ بلکه فوراً خطای محلی صادر میشود تا سرور مقصد فرصت بازیابی داشته باشد.

| معیار مقایسه | HttpClient سنتی (Transient) | HttpClient استاتیک (Singleton) | IHttpClientFactory (پیشنهادی) |
| مدیریت سوکتها | ضعیف (باعث اشباع سوکت میشود) | عالی (فقط از یک سوکت استفاده میکند) | عالی (از استخر هاندرها بهینهسازی میکند) |
| واکنش به تغییرات DNS | بله (برای هر نمونه مجدد فراخوانی میشود) | خیر (تغییرات IP را متوجه نمیشود) | بله (با منقضی کردن هاندرها در بازه ۲ دقیقهای) |
| قابلیت پیکربندی متمرکز | خیر | خیر | بله (از طریق روشهای Named و Typed) |
| یکپارچهسازی با Polly / لاگین | نیاز به کدنویسی دستی گسترده دارد | دشوار و غیرقابل انعطاف | کاملاً بومی و ساختاریافته |
به عنوان یک مهندس ارشد نرمافزار، هنگام استفاده از IHttpClientFactory باید به جزییات زیرساختی زیر توجه ویژهای داشته باشید:
تغییر زمان انقضای هاندر (Handler Lifetime): زمان پیشفرض ۲ دقیقه است. اگر سرویسهای شما در محیط پایداری هستند، تغییر آن به مقادیر دیگر با متد SetHandlerLifetime(TimeSpan.FromMinutes(5)) امکانپذیر است.
اجتناب از تزریق کلاینتهای تایپشده در کامپوننتهای Singleton: از آنجا که کلاینتهای تایپشده خودشان از ساختار تزریق وابستگی عبور میکنند، اگر آنها را درون یک سرویس استاتیک یا Singleton تزریق کنید، کلاینت اسیر آن لایفتایم بزرگتر شده (Captive Dependency) و باز هم با مشکل عدم بهروزرسانی DNS مواجه خواهید شد.
استفاده از SocketsHttpHandler در پلتفرمهای مدرن: در داتنتهای مدرن، کارخانه به طور خودکار از SocketsHttpHandler استفاده میکند که عملکرد به مراتب بالاتری نسبت به لایههای قدیمی سیستمعامل دارد.
ابزار IHttpClientFactory صرفاً یک ابزار کمکی ساده نیست، بلکه یک ضرورت معماری برای سیستمهای با ترافیک بالا و توزیعشده است. این ابزار توازن فوقالعادهای میان "صرفهجویی در مصرف سوکتهای سیستمعامل" و "پویایی در برابر تغییرات توپولوژی شبکه و DNS" ایجاد میکند. با ترکیب این ابزار با لایههای پیام سفارشی و استراتژیهای پایداری Polly، برنامههای داتنت شما آمادگی لازم برای مواجهه با سختترین شرایط ترافیکی شبکه را خواهند داشت.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.