اگر در کدهای خود قطعه کد ذیل را دارید:
using(var client = new HttpClient())
{
// do something with http client
}
استفادهی از using در اینجا، نهتنها غیرضروری و اشتباه است، بلکه سبب از کار افتادن زود هنگام برنامهی شما با صدور استثنای ذیل خواهد شد:
Unable to connect to the remote server
System.Net.Sockets.SocketException: Only one usage of each socket address (protocol/network address/port) is normally permitted.
HttpClient خود را Dispose نکنید
کلاس HttpClient اینترفیس IDisposable را پیاده سازی میکند. بنابراین روش استفادهی اصولی آن باید به صورت ذیل و با پیاده سازی خودکار رهاسازی منابع مرتبط با آن باشد:
using (var client = new HttpClient())
{
var result = await client.GetAsync("http://example.com/");
}
اما در این حال فرض کنید به همین روش تعدادی درخواست را ارسال کردهاید:
for (int i = 0; i < 10; i++)
{
using (var client = new HttpClient())
{
var result = await client.GetAsync("http://example.com/");
Console.WriteLine(result.StatusCode);
}
}
مشکل این روش، در ایجاد سوکتهای متعددی است که حتی پس از بسته شدن برنامه نیز باز، باقی خواهند ماند:
TCP 192.168.1.6:13996 93.184.216.34:http TIME_WAIT
TCP 192.168.1.6:13997 93.184.216.34:http TIME_WAIT
TCP 192.168.1.6:13998 93.184.216.34:http TIME_WAIT
TCP 192.168.1.6:13999 93.184.216.34:http TIME_WAIT
TCP 192.168.1.6:14000 93.184.216.34:http TIME_WAIT
TCP 192.168.1.6:14001 93.184.216.34:http TIME_WAIT
TCP 192.168.1.6:14002 93.184.216.34:http TIME_WAIT
TCP 192.168.1.6:14003 93.184.216.34:http TIME_WAIT
TCP 192.168.1.6:14004 93.184.216.34:http TIME_WAIT
TCP 192.168.1.6:14005 93.184.216.34:http TIME_WAIT
این یک نمونهی خروجی برنامهی فوق، توسط دستور netstat «پس از بسته شدن کامل برنامه» است.
بنابراین اگر برنامهی شما تعداد زیادی کاربر دارد و یا تعداد زیادی درخواست را به روش فوق ارسال میکند، سیستم عامل به حد اشباع ایجاد سوکتهای جدید خواهد رسید.
این مشکل نیز ارتباطی به طراحی این کلاس و یا زبان #C و حتی استفادهی از using نیز ندارد. این رفتار، رفتار معمول سیستم عامل، با سوکتهای ایجاد شدهاست. TIME_WAIT ایی را که در اینجا ملاحظه میکنید، به معنای بسته شدن اتصال از طرف برنامهی ما است؛ اما سیستم عامل هنوز منتظر نتیجهی نهایی، از طرف دیگر اتصال است که آیا قرار است بستهی TCP ایی را دریافت کند یا خیر و یا شاید در بین راه تاخیری وجود داشتهاست. برای نمونه ویندوز به مدت 240 ثانیه یک اتصال را در این حالت حفظ خواهد کرد، که مقدار آن نیز در اینجا تنظیم میشود:
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\TcpTimedWaitDelay]
بنابراین روش توصیه شدهی کار با HttpClient، داشتن یک وهلهی سراسری از آن در برنامه و عدم Dispose آن است. HttpClient نیز thread-safe طراحی شدهاست و دسترسی به یک شیء سراسری آن در برنامههای چند ریسمانی مشکلی را ایجاد نمیکند. همچنین Dispose آن نیز غیرضروری است و پس از پایان برنامه به صورت خودکار توسط سیستم عامل انجام خواهد شد.
تمام اجزای HttpClient به صورت Thread-safe طراحی نشدهاند
تا اینجا به این نتیجه رسیدیم که روش صحیح کار کردن با HttpClient، نیاز به داشتن یک وهلهی Singleton از آنرا در سراسر برنامه دارد و Dispose صریح آن، بجز اشباع سوکتهای سیستم عامل و ناپایدار کردن تمام برنامههایی که از آن سرویس میگیرند، حاصلی را به همراه نخواهد داشت. در این بین مطابق مستندات HttpClient، استفادهی از متدهای ذیل این کلاس thread-safe هستند:
CancelPendingRequests
DeleteAsync
GetAsync
GetByteArrayAsync
GetStreamAsync
GetStringAsync
PostAsync
PutAsync
SendAsync
اما تغییر این خواص در کلاس HttpClient به هیچ عنوان thread-safe نبوده و در برنامههای چند ریسمانی و چند کاربری، مشکل ساز میشوند:
BaseAddress
DefaultRequestHeaders
MaxResponseContentBufferSize
Timeout
بنابراین در طراحی کلاس مدیریت کنندهی HttpClient برنامهی خود نیاز است به ازای هر BaseAddress، یک HttpClient خاص آنرا ایجاد کرد و HttpClientهای سراسری نمیتوانند BaseAddressهای خود را نیز به اشتراک گذاشته و تغییری را در آن ایجاد کنند.
استفادهی سراسری و مجدد از HttpClient، تغییرات DNS را متوجه نمیشود
با طراحی یک کلاس مدیریت کنندهی سراسری HttpClient با طول عمر Singelton، به یک مشکل دیگر نیز برخواهیم خورد: چون در اینجا از اتصالات، استفادهی مجدد میشوند، دیگر تغییرات DNS را لحاظ نخواهند کرد.
برای حل این مشکل، در زمان ایجاد یک HttpClient سراسری، به ازای یک BaseAddress مشخص، باید از ServicePointManager کوئری گرفته و زمان اجارهی اتصال آنرا دقیقا مشخص کنیم:
var sp = ServicePointManager.FindServicePoint(new Uri("http://thisisasample.com"));
sp.ConnectionLeaseTimeout = 60*1000; //In milliseconds
با اینکار هرچند هنوز هم از اتصالات استفادهی مجدد میشود، اما این استفادهی مجدد، نامحدود نبوده و مدت معینی را پیدا میکند.
طراحی یک کلاس، برای مدیریت سراسری وهلههای HttpClient
تا اینجا به صورت خلاصه به نکات ذیل رسیدیم:
- HttpClient باید به صورت یک وهلهی سراسری Singleton مورد استفاده قرار گیرد. هر وهله سازی مجدد آن 35ms زمان میبرد.
- Dispose یک HttpClient غیرضروری است.
- HttpClient تقریبا thread safe طراحی شدهاست؛ اما تعدادی از خواص آن مانند BaseAddress اینگونه نیستند.
- برای رفع مشکل اتصالات چسبنده (اتصالاتی که هیچگاه پایان نمییابند)، نیاز است timeout آنرا تنظیم کرد.
بنابراین بهتر است این نکات را در یک کلاس به صورت ذیل کپسوله کنیم:
using System;
using System.Collections.Generic;
using System.Net.Http;
namespace HttpClientTips
{
public interface IHttpClientFactory : IDisposable
{
HttpClient GetOrCreate(
Uri baseAddress,
IDictionary<string, string> defaultRequestHeaders = null,
TimeSpan? timeout = null,
long? maxResponseContentBufferSize = null,
HttpMessageHandler handler = null);
}
}
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading;
namespace HttpClientTips
{
/// <summary>
/// Lifetime of this class should be set to `Singleton`.
/// </summary>
public class HttpClientFactory : IHttpClientFactory
{
// 'GetOrAdd' call on the dictionary is not thread safe and we might end up creating the HttpClient more than
// once. To prevent this Lazy<> is used. In the worst case multiple Lazy<> objects are created for multiple
// threads but only one of the objects succeeds in creating the HttpClient.
private readonly ConcurrentDictionary<Uri, Lazy<HttpClient>> _httpClients =
new ConcurrentDictionary<Uri, Lazy<HttpClient>>();
private const int ConnectionLeaseTimeout = 60 * 1000; // 1 minute
public HttpClientFactory()
{
// Default is 2 minutes: https://msdn.microsoft.com/en-us/library/system.net.servicepointmanager.dnsrefreshtimeout(v=vs.110).aspx
ServicePointManager.DnsRefreshTimeout = (int)TimeSpan.FromMinutes(1).TotalMilliseconds;
// Increases the concurrent outbound connections
ServicePointManager.DefaultConnectionLimit = 1024;
}
public HttpClient GetOrCreate(
Uri baseAddress,
IDictionary<string, string> defaultRequestHeaders = null,
TimeSpan? timeout = null,
long? maxResponseContentBufferSize = null,
HttpMessageHandler handler = null)
{
return _httpClients.GetOrAdd(baseAddress,
uri => new Lazy<HttpClient>(() =>
{
// Reusing a single HttpClient instance across a multi-threaded application means
// you can't change the values of the stateful properties (which are not thread safe),
// like BaseAddress, DefaultRequestHeaders, MaxResponseContentBufferSize and Timeout.
// So you can only use them if they are constant across your application and need their own instance if being varied.
var client = handler == null ? new HttpClient { BaseAddress = baseAddress } :
new HttpClient(handler, disposeHandler: false) { BaseAddress = baseAddress };
setRequestTimeout(timeout, client);
setMaxResponseBufferSize(maxResponseContentBufferSize, client);
setDefaultHeaders(defaultRequestHeaders, client);
setConnectionLeaseTimeout(baseAddress, client);
return client;
},
LazyThreadSafetyMode.ExecutionAndPublication)).Value;
}
public void Dispose()
{
foreach (var httpClient in _httpClients.Values)
{
httpClient.Value.Dispose();
}
}
private static void setConnectionLeaseTimeout(Uri baseAddress, HttpClient client)
{
// This ensures connections are used efficiently but not indefinitely.
client.DefaultRequestHeaders.ConnectionClose = false; // keeps the connection open -> more efficient use of the client
ServicePointManager.FindServicePoint(baseAddress).ConnectionLeaseTimeout = ConnectionLeaseTimeout; // ensures connections are not used indefinitely.
}
private static void setDefaultHeaders(IDictionary<string, string> defaultRequestHeaders, HttpClient client)
{
if (defaultRequestHeaders == null)
{
return;
}
foreach (var item in defaultRequestHeaders)
{
client.DefaultRequestHeaders.Add(item.Key, item.Value);
}
}
private static void setMaxResponseBufferSize(long? maxResponseContentBufferSize, HttpClient client)
{
if (maxResponseContentBufferSize.HasValue)
{
client.MaxResponseContentBufferSize = maxResponseContentBufferSize.Value;
}
}
private static void setRequestTimeout(TimeSpan? timeout, HttpClient client)
{
if (timeout.HasValue)
{
client.Timeout = timeout.Value;
}
}
}
}
در اینجا به ازای هر baseAddress جدید، یک HttpClient خاص آن ایجاد میشود تا در کل برنامه مورد استفادهی مجدد قرار گیرد. برای مدیریت thread-safe ایجاد HttpClientها نیز از نکتهی مطلب «
الگویی برای مدیریت دسترسی همزمان به ConcurrentDictionary» استفاده شدهاست. همچنین نکات تنظیم ConnectionLeaseTimeout و سایر خواص غیر thread-safe کلاس HttpClient نیز در اینجا لحاظ شدهاند.
پس از تدارک این کلاس، نحوهی معرفی آن به سیستم باید به صورت Singleton باشد. برای مثال اگر از ASP.NET Core استفاده میکنید، آنرا به صورت ذیل ثبت کنید:
namespace HttpClientTips.Web
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHttpClientFactory, HttpClientFactory>();
services.AddMvc();
}
اکنون، یک نمونه، نحوهی استفادهی از اینترفیس IHttpClientFactory تزریقی به صورت ذیل میباشد:
namespace HttpClientTips.Web.Controllers
{
public class HomeController : Controller
{
private readonly IHttpClientFactory _httpClientFactory;
public HomeController(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<IActionResult> Index()
{
var host = new Uri("http://localhost:5000");
var httpClient = _httpClientFactory.GetOrCreate(host);
var responseMessage = await httpClient.GetAsync("home/about").ConfigureAwait(false);
var responseContent = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
return Content(responseContent);
}
سرویس IHttpClientFactory یک HttpClient را به ازای host درخواستی ایجاد کرده و در طول عمر برنامه از آن استفادهی مجدد میکند. به همین جهت دیگر مشکل اشباع سوکتها در این سیستم رخ نخواهند داد.
برای مطالعهی بیشتر You're using HttpClient wrong and it is destabilizing your software Disposable, Finalizers, and HttpClient Using HttpClient as it was intended (because you’re not) Singleton HttpClient? Beware of this serious behaviour and how to fix it Beware of the .NET HttpClient Effectively Using HttpClient