天天看點

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

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

一、前言

在我們開發當中經常需要向特定URL位址發送Http請求操作,在.net core 中對httpClient使用不當會造成災難性的問題,這篇文章主要來分享.net core中通過IHttpClientFactory 工廠來使用HttpClient的正确打開方式。

二、HttpClient使用中的那些坑

2.1 錯誤使用

using(var client = new HttpClient())

我們可以先來做一個簡單的測試,代碼如下:

public async Task 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();

靜态單例模式雖然可以解決上面問題,但是會帶來另外一個問題:

DNS變更會導緻不能解析,DNS不會重新加載,需要重新開機才能變更(有興趣的大佬可以去嘗試一下)

三、正确使用及源碼分析

HttpClientFactory 以子產品化、可命名、可配置、彈性方式重建了 HttpClient 的使用方式: 由 DI 架構注入 IHttpClientFactory 工廠;由工廠建立 HttpClient 并從内部的 Handler 池配置設定請求 Handler。

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

services.AddControllers();

services.AddHttpClient();

調用代碼如下:

private readonly IHttpClientFactory _clientFactory;

public FirstController(IHttpClientFactory clientFactory)

{

_clientFactory = clientFactory;           

///

public async Task GetBaiduAsync(string url)

var client = _clientFactory.CreateClient();
var result = await client.GetStringAsync(url);
return result;           

代碼中通過IHttpClientFactory 中的CreateClient()方法進行建立一個HttpClient 對象,但是沒有看到有釋放資源的動作,那它是怎麼釋放的呢?

我們來看看它的主要源代碼

/// Creates a new using the default configuration.

/// The .

/// An configured using the default configuration.

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

希望這篇文章對你有幫助,如果對你有幫助請點個推薦,感謝!

如果您認為這篇文章還不錯或者有所收獲,您可以點選右下角的【推薦】按鈕精神支援,因為這種支援是我繼續寫作,分享的最大動力!

作者:Jlion

原文位址

https://www.cnblogs.com/jlion/p/12813692.html

繼續閱讀