.net core HttpClient 使用之掉坑解析(一)

1、前言

在咱們開發當中常常須要向特定URL地址發送Http請求操做,在.net core 中對httpClient使用不當會形成災難性的問題,這篇文章主要來分享.net core中經過IHttpClientFactory 工廠來使用HttpClient的正確打開方式。html

2、HttpClient使用中的那些坑

2.1 錯誤使用

using(var client = new HttpClient())

咱們能夠先來作一個簡單的測試,代碼以下:windows

public async Task<string> GetBaiduListAsync(string url)
 {
     var html = "";
     for (var i = 0; i < 10; i++)
     {
          using (var client = new System.Net.Http.HttpClient())
          {
             var result=await client.GetStringAsync(url);
             html += result;
           }
     }
     return html;
 }

運行項目輸出結果後,經過netstate查看下TCP鏈接狀況:

雖然項目已經運行結束,可是鏈接依然存在,狀態爲" TIME_WAIT"(繼續等待看是否還有延遲的包會傳輸過來;默認在windows下,TIME_WAIT狀態將會使系統將會保持該鏈接 240s。
在高併發的狀況下,鏈接來不及釋放,socket被耗盡,耗盡以後就會出現喜聞樂見的一個錯誤:網絡

錯誤緣由:

對象所佔用資源應該確保及時被釋放掉,可是,對於網絡鏈接而言,這是錯誤的,緣由有以下:併發

  • 網絡鏈接是須要耗費必定時間的,頻繁開啓與關閉鏈接,性能會受影響;
  • 開啓網絡鏈接時會佔用底層socket資源,但在HttpClient調用其自己的Dispose方法時,並不能馬上釋放該資源,這意味着你的程序可能會由於耗盡鏈接資源而產生災難性的問題。

對於上面的錯誤緣由,你們可能會想到使用靜態單例模式的HttpClient,以下:框架

private static HttpClient Client = new HttpClient();

靜態單例模式雖然能夠解決上面問題,可是會帶來另一個問題:socket

  • DNS變動會致使不能解析,DNS不會從新加載,須要重啓才能變動(有興趣的大佬能夠去嘗試一下)

3、正確使用及源碼分析

HttpClientFactory 以模塊化、可命名、可配置、彈性方式重建了 HttpClient 的使用方式: 由 DI 框架注入 IHttpClientFactory 工廠;由工廠建立 HttpClient 並從內部的 Handler 池分配請求 Handler。async

.net core 2.1 開始引入了IHttpClientFactory 工廠類來自動管理IHttpClientFactory 類的建立和資源釋放,能夠經過Ioc 注入方式進行使用,代碼以下:ide

services.AddControllers();
services.AddHttpClient();

調用代碼以下:模塊化

private readonly IHttpClientFactory _clientFactory;

public FirstController(IHttpClientFactory clientFactory)
{
    _clientFactory = clientFactory;
}


/// <summary>
/// 
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public async Task<string> GetBaiduAsync(string url)
{
    var client = _clientFactory.CreateClient();
    var result = await client.GetStringAsync(url);
    return result;
}

代碼中經過IHttpClientFactory 中的CreateClient()方法進行建立一個HttpClient 對象,可是沒有看到有釋放資源的動做,那它是怎麼釋放的呢?
咱們來看看它的主要源代碼高併發

/// <summary>
/// Creates a new <see cref="HttpClient"/> using the default configuration.
/// </summary>
/// <param name="factory">The <see cref="IHttpClientFactory"/>.</param>
/// <returns>An <see cref="HttpClient"/> configured using the default configuration.</returns>
public static HttpClient CreateClient(this IHttpClientFactory factory)
{
     if (factory == null)
     {
        throw new ArgumentNullException(nameof(factory));
     }

     return factory.CreateClient(Options.DefaultName);
}

public HttpClient CreateClient(string name)
{
     if (name == null)
     {
          throw new ArgumentNullException(nameof(name));
     }

     var handler = CreateHandler(name);
     var client = new HttpClient(handler, disposeHandler: false);

     var options = _optionsMonitor.Get(name);
     for (var i = 0; i < options.HttpClientActions.Count; i++)
     {
         options.HttpClientActions[i](client);
     }

     return client;
}

public HttpMessageHandler CreateHandler(string name)
{
     if (name == null)
     {
         throw new ArgumentNullException(nameof(name));
     }

     var entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value;

     StartHandlerEntryTimer(entry);

     return entry.Handler;
}

代碼中能夠看到建立HttpClent 時會先建立HttpMessageHandler對象,而CreateHandler 方法中調用了StartHandlerEntryTimer方法,該方法主要時啓動清理釋放定時器方法,核心代碼以下:

public DefaultHttpClientFactory(
            IServiceProvider services,
            IServiceScopeFactory scopeFactory,
            ILoggerFactory loggerFactory,
            IOptionsMonitor<HttpClientFactoryOptions> optionsMonitor,
            IEnumerable<IHttpMessageHandlerBuilderFilter> filters)
        {
            if (services == null)
            {
                throw new ArgumentNullException(nameof(services));
            }

            if (scopeFactory == null)
            {
                throw new ArgumentNullException(nameof(scopeFactory));
            }

            if (loggerFactory == null)
            {
                throw new ArgumentNullException(nameof(loggerFactory));
            }

            if (optionsMonitor == null)
            {
                throw new ArgumentNullException(nameof(optionsMonitor));
            }

            if (filters == null)
            {
                throw new ArgumentNullException(nameof(filters));
            }

            _services = services;
            _scopeFactory = scopeFactory;
            _optionsMonitor = optionsMonitor;
            _filters = filters.ToArray();

            _logger = loggerFactory.CreateLogger<DefaultHttpClientFactory>();

            // case-sensitive because named options is.
            _activeHandlers = new ConcurrentDictionary<string, Lazy<ActiveHandlerTrackingEntry>>(StringComparer.Ordinal);
            _entryFactory = (name) =>
            {
                return new Lazy<ActiveHandlerTrackingEntry>(() =>
                {
                    return CreateHandlerEntry(name);
                }, LazyThreadSafetyMode.ExecutionAndPublication);
            };

            _expiredHandlers = new ConcurrentQueue<ExpiredHandlerTrackingEntry>();
            _expiryCallback = ExpiryTimer_Tick;

            _cleanupTimerLock = new object();
            _cleanupActiveLock = new object();
        }

        // Internal for tests
        internal void ExpiryTimer_Tick(object state)
        {
            var active = (ActiveHandlerTrackingEntry)state;

            // The timer callback should be the only one removing from the active collection. If we can't find
            // our entry in the collection, then this is a bug.
            var removed = _activeHandlers.TryRemove(active.Name, out var found);
            Debug.Assert(removed, "Entry not found. We should always be able to remove the entry");
            Debug.Assert(object.ReferenceEquals(active, found.Value), "Different entry found. The entry should not have been replaced");

            // At this point the handler is no longer 'active' and will not be handed out to any new clients.
            // However we haven't dropped our strong reference to the handler, so we can't yet determine if
            // there are still any other outstanding references (we know there is at least one).
            //
            // We use a different state object to track expired handlers. This allows any other thread that acquired
            // the 'active' entry to use it without safety problems.
            var expired = new ExpiredHandlerTrackingEntry(active);
            _expiredHandlers.Enqueue(expired);

            Log.HandlerExpired(_logger, active.Name, active.Lifetime);

            StartCleanupTimer();
        }

        // Internal so it can be overridden in tests
        internal virtual void StartHandlerEntryTimer(ActiveHandlerTrackingEntry entry)
        {
            entry.StartExpiryTimer(_expiryCallback);
        }

從微軟源碼分析,HttpClient繼承自HttpMessageInvoker,而HttpMessageInvoker實質就是HttpClientHandler。

HttpClientFactory 建立的HttpClient,也便是HttpClientHandler,只是這些個HttpClient被放到了「池子」中,工廠每次在create的時候會自動判斷是新建仍是複用。(默認生命週期爲2min)。 但願這篇文章對你有幫助,若是對你有幫助請點個推薦,感謝!

相關文章
相關標籤/搜索