.net core HttpClient 使用之掉坑解析(一)
阿新 • • 發佈:2020-05-13
## 一、前言
在我們開發當中經常需要向特定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連線情況:
![](https://img2020.cnblogs.com/blog/824291/202005/824291-20200503114621000-310046717.png)
雖然專案已經執行結束,但是連線依然存在,狀態為" TIME_WAIT"(繼續等待看是否還有延遲的包會傳輸過來;預設在windows下,`TIME_WAIT`狀態將會使系統將會保持該連線 240s。
在高併發的情況下,連線來不及釋放,socket被耗盡,耗盡之後就會出現喜聞樂見的一個錯誤:
#### 錯誤原因:
物件所佔用資源應該確保及時被釋放掉,但是,對於網路連線而言,這是錯誤的,原因有如下:
- 網路連線是需要耗費一定時間的,頻繁開啟與關閉連線,效能會受影響;
- 開啟網路連線時會佔用底層socket資源,但在HttpClient呼叫其本身的Dispose方法時,並不能立刻釋放該資源,這意味著你的程式可能會因為耗盡連線資源而產生災難性的問題。
對於上面的錯誤原因,大家可能會想到使用靜態單例模式的HttpClient,如下:
```
private static HttpClient Client = new HttpClient();
```
靜態單例模式雖然可以解決上面問題,但是會帶來另外一個問題:
- DNS變更會導致不能解析,DNS不會重新載入,需要重啟才能變更(有興趣的大佬可以去嘗試一下)
## 三、正確使用及原始碼分析
![](https://img2020.cnblogs.com/blog/824291/202005/824291-20200501154248841-1287450278.jpg)
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 optionsMonitor,
IEnumerable 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();
// case-sensitive because named options is.
_activeHandlers = new ConcurrentDictionary(StringComparer.Ordinal);
_entryFactory = (name) =>
{
return new Lazy(() =>
{
return CreateHandlerEntry(name);
}, LazyThreadSafetyMode.ExecutionAndPublication);
};
_expiredHandlers = new ConcurrentQueue();
_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)。
希望這篇文章對你有幫助,如果對你有幫助請點個推薦,感謝!