当需要向某特定 URL 地址发送 HTTP 请求并得到相应响应时, 通常会用到 HttpClient 类. 该类包含了众多有用的方法, 可以满足绝大多数的需求. 但是如果对其使用不当时, 可能会出现意想不到的事情.
博客园官方团队就遇上过这样的问题, 国外博主也记录过类似的情况, YOU'RE USING HTTPCLIENT WRONG AND IT IS DESTABILIZING YOUR SOFTWARE https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/ .
究其缘由是一句看似正确的代码引起的:
using(var client = new HttpClient())
对象所占用资源应该确保及时被释放掉, 但是, 对于网络连接而言, 这是错误的.
原因有二, 网络连接是需要耗费一定时间的, 频繁开启与关闭连接, 性能会受影响; 再者, 开启网络连接时会占用底层 socket 资源, 但在 HttpClient 调用其本身的 Dispose 方法时, 并不能立刻释放该资源, 这意味着你的程序可能会因为耗尽连接资源而产生预期之外的异常.
所以比较好的解决方法是延长 HttpClient 对象的使用寿命, 比如对其建一个静态的对象:
private static HttpClient Client = new HttpClient();
但从程序员的角度来看, 这样的代码或许不够优雅.
所以在. NET Core 中引入了新的 HttpClientFactory 类.
它的用法很简单, 首先是对其进行 Ioc 的注册:
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddHttpClient();
- services.AddMvc();
- }
然后通过 IHttpClientFactory 创建一个 HttpClient 对象, 之后的操作如旧, 但不需要担心其内部资源的释放:
- public class MyController : Controller
- {
- IHttpClientFactory _httpClientFactory;
- public MyController(IHttpClientFactory httpClientFactory)
- {
- _httpClientFactory = httpClientFactory;
- }
- public IActionResult Index()
- {
- var client = _httpClientFactory.CreateClient();
- var result = client.GetStringAsync("http://myurl/");
- return View();
- }
- }
第一眼瞧去, 可能不明白 AddHttpClient 方法与 IHttpClientFactory 有什么关系, 但查到其源码后就能一目了然:
- public static IServiceCollection AddHttpClient(this IServiceCollection services)
- {
- if (services == null)
- {
- throw new ArgumentNullException(nameof(services));
- }
- services.AddLogging();
- services.AddOptions();
- //
- // Core abstractions
- //
- services.TryAddTransient<HttpMessageHandlerBuilder, DefaultHttpMessageHandlerBuilder>();
- services.TryAddSingleton<IHttpClientFactory, DefaultHttpClientFactory>();
- //
- // Typed Clients
- //
- services.TryAdd(ServiceDescriptor.Singleton(typeof(ITypedHttpClientFactory<>), typeof(DefaultTypedHttpClientFactory<>)));
- //
- // Misc infrastructure
- //
- services.TryAddEnumerable(ServiceDescriptor.Singleton<IHttpMessageHandlerBuilderFilter, LoggingHttpMessageHandlerBuilderFilter>());
- return services;
- }
它的内部为 IHttpClientFactory 接口绑定了 DefaultHttpClientFactory 类.
再看 IHttpClientFactory 接口中关键的 CreateClient 方法:
- public HttpClient CreateClient(string name)
- {
- if (name == null)
- {
- throw new ArgumentNullException(nameof(name));
- }
- var entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value;
- var client = new HttpClient(entry.Handler, disposeHandler: false);
- StartHandlerEntryTimer(entry);
- var options = _optionsMonitor.Get(name);
- for (var i = 0; i <options.HttpClientActions.Count; i++)
- {
- options.HttpClientActions[i](client);
- }
- return client;
- }
HttpClient 的创建不再是简单的 new HttpClient(), 而是传入了两个参数: HttpMessageHandler handler 与 bool disposeHandler.disposeHandler 参数为 false 值时表示要重用内部的 handler 对象. handler 参数则从上一句的代码可以看出是以名称为键值从一字典中取出, 又因为 DefaultHttpClientFactory 类是通过 TryAddSingleton 方法注册的, 也就意味着其为单例, 那么这个内部字典便是唯一的, 每个键值对应的 ActiveHandlerTrackingEntry 对象也是唯一, 该对象内部中就包含着 handler.
下一句代码
StartHandlerEntryTimer(entry);
开启了 ActiveHandlerTrackingEntry 对象的过期计时处理. 默认过期时间是 2 分钟.
- 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();
- }
先是将 ActiveHandlerTrackingEntry 对象传入新的 ExpiredHandlerTrackingEntry 对象.
- public ExpiredHandlerTrackingEntry(ActiveHandlerTrackingEntry other)
- {
- Name = other.Name;
- _livenessTracker = new WeakReference(other.Handler);
- InnerHandler = other.Handler.InnerHandler;
- }
在其构造方法内部, handler 对象通过弱引用方式关联着, 不会影响其被 GC 释放.
然后新建的 ExpiredHandlerTrackingEntry 对象被放入专用的队列.
最后开始清理工作, 定时器的时间间隔设定为每 10 秒一次.
- internal void CleanupTimer_Tick(object state)
- {
- // Stop any pending timers, we'll restart the timer if there's anything left to process after cleanup.
- //
- // With the scheme we're using it's possible we could end up with some redundant cleanup operations.
- // This is expected and fine.
- //
- // An alternative would be to take a lock during the whole cleanup process. This isn't ideal because it
- // would result in threads executing ExpiryTimer_Tick as they would need to block on cleanup to figure out
- // whether we need to start the timer.
- StopCleanupTimer();
- try
- {
- if (!Monitor.TryEnter(_cleanupActiveLock))
- {
- // We don't want to run a concurrent cleanup cycle. This can happen if the cleanup cycle takes
- // a long time for some reason. Since we're running user code inside Dispose, it's definitely
- // possible.
- //
- // If we end up in that position, just make sure the timer gets started again. It should be cheap
- // to run a 'no-op' cleanup.
- StartCleanupTimer();
- return;
- }
- var initialCount = _expiredHandlers.Count;
- Log.CleanupCycleStart(_logger, initialCount);
- var stopwatch = ValueStopwatch.StartNew();
- var disposedCount = 0;
- for (var i = 0; i < initialCount; i++)
- {
- // Since we're the only one removing from _expired, TryDequeue must always succeed.
- _expiredHandlers.TryDequeue(out var entry);
- Debug.Assert(entry != null, "Entry was null, we should always get an entry back from TryDequeue");
- if (entry.CanDispose)
- {
- try
- {
- entry.InnerHandler.Dispose();
- disposedCount++;
- }
- catch (Exception ex)
- {
- Log.CleanupItemFailed(_logger, entry.Name, ex);
- }
- }
- else
- {
- // If the entry is still live, put it back in the queue so we can process it
- // during the next cleanup cycle.
- _expiredHandlers.Enqueue(entry);
- }
- }
- Log.CleanupCycleEnd(_logger, stopwatch.GetElapsedTime(), disposedCount, _expiredHandlers.Count);
- }
- finally
- {
- Monitor.Exit(_cleanupActiveLock);
- }
- // We didn't totally empty the cleanup queue, try again later.
- if (_expiredHandlers.Count> 0)
- {
- StartCleanupTimer();
- }
- }
上述方法核心是判断是否 handler 对象已经被 GC, 如果是的话, 则释放其内部资源, 即网络连接.
回到最初创建 HttpClient 的代码, 会发现并没有传入任何 name 参数值. 这是受益于 HttpClientFactoryExtensions 类的扩展方法.
- public static HttpClient CreateClient(this IHttpClientFactory factory)
- {
- if (factory == null)
- {
- throw new ArgumentNullException(nameof(factory));
- }
- return factory.CreateClient(Options.DefaultName);
- }
在传入 name 参数值时, 还可以传入一些配置数据:
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddHttpClient("github", c =>
- {
- c.BaseAddress = new Uri("https://api.github.com/");
- c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
- c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
- });
- services.AddHttpClient();
- }
来源: https://www.cnblogs.com/kenwoo/p/9333042.html