千家信息网

.NET Core中的HttpClientFactory类怎么用

发表于:2025-11-13 作者:千家信息网编辑
千家信息网最后更新 2025年11月13日,小编给大家分享一下.NET Core中的HttpClientFactory类怎么用,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一
千家信息网最后更新 2025年11月13日.NET Core中的HttpClientFactory类怎么用

小编给大家分享一下.NET Core中的HttpClientFactory类怎么用,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!

一、HttpClient使用

在C#中,如果我们需要向某特定的URL地址发送Http请求的时候,通常会用到HttpClient类。会将HttpClient包裹在using内部进行声明和初始化,如下面的代码:

using (var httpClient = new HttpClient()){    // 逻辑处理代码}

HttpClient类包含了许多有用的方法,使用上面的代码,可以满足绝大多数的需求,但是如果对其使用不当时,可能会出现意想不到的事情。

上面代码的技术范点:当你使用继承了IDisposable接口的对象时,建议在using代码块中声明和初始化,当using代码段执行完成后,会自动释放该对象而不需要手动进行显示Dispose操作。

对象所占用资源应该确保及时被释放掉,但是,对于网络连接而言,这是错误的。具体原因有下面两点:

  • 网络连接是需要耗费一定时间的,频繁开启与关闭连接,性能会受到影响。

  • 开启网络连接时会占用低层socket资源,但在HttpClient调用其本身的Dispose方法时,并不能立即释放该资源,这意味着你的程序可能会因为耗尽连接资源而产生预期之外的异常。

看下面一段代码

using System;using System.Collections.Generic;using System.Linq;using System.Net.Http;using System.Text;using System.Threading.Tasks;namespace HttpClientDemo{    class Program    {        static void Main(string[] args)        {            //using (var httpClient = new HttpClient())            //{            //    // 逻辑处理代码            //}            HttpAsync();            Console.WriteLine("Hello World!");            Console.Read();        }        public static async void HttpAsync()        {            for (int i = 0; i < 10; i++)            {                using (var client = new HttpClient())                {                    var result = await client.GetAsync("http://www.baidu.com");                    Console.WriteLine($"{i}:{result.StatusCode}");                }            }        }    }}

运行项目输出结果后,通过netstate查看下TCP连接情况,会发现连接依然存在,状态为"TIME_WAIT"(继续等待看是否还有延迟的包会传输过来)。

这里就会出现一个坑:在高并发的情况下,连接来不及释放,socket连接被耗尽,耗尽之后就会出现错误。就是会出现"各种套接字问题"。

那么如何解决这个问题呢?比较好的解决方法是延长HttpClient对象的使用寿命,实现HttpClient对象的复用,比如对其建一个静态的对象:

private static HttpClient Client = new HttpClient();

我们使用这种方式优化上面的代码

using System;using System.Net.Http;namespace HttpClientDemo{    class Program    {        private static readonly HttpClient _client = new HttpClient();        static void Main(string[] args)        {            HttpAsync();            Console.WriteLine("Hello World");            Console.ReadKey();        }        public static async void HttpAsync()        {            for (int i = 0; i < 10; i++)            {                var result = await _client.GetAsync("http://www.baidu.com");                Console.WriteLine($"{i}:{result.StatusCode}");            }        }    }}

这样调整HttpClient的引用后,虽然可以解决一些问题,但是仍然存在一些问题:

  • 因为是复用的HttpClient,那么一些公共的设置就没办法灵活的调整,如请求头的自定义。

  • 因为HttpClient请求每个url时,会缓存url对应的主机ip,从而会导致DNS更新失效。

为了解决这些问题,在.NET Core 2.1中引入了新的HttpClientFactory类。

二、HttpClientFactory使用

微软在.NET Core 2.1中新引入了HttpClientFactory类,具有如下的优势:

  • HttpClientFactory很高效,可以最大程度上节省系统的sock而。

  • Factory,顾名思义HttpClientfactory就是HttpClient的工厂,内部已经帮我们处理好了对HttpClient的管理,不需要我们人工进行对象释放,同时,支持自定义请求头、支持DNS更新等。

我们用一个ASP.NET Core的程序作为示例,它的用法非常简单,首先是对其进行IOC注册:

public void ConfigureServices(IServiceCollection services){    // 注入HttpClient    services.AddHttpClient("client_1", config =>  //这里指定的name=client_1,可以方便我们后期服用该实例    {        config.BaseAddress = new Uri("http://www.baidu.com");        config.DefaultRequestHeaders.Add("header_1", "header_1");    });    services.AddHttpClient("client_2", config =>    {        config.BaseAddress = new Uri("https://www.qq.com/");        config.DefaultRequestHeaders.Add("header_2", "header_2");    });    services.AddHttpClient();    services.AddControllers();}

然后在控制器里面通过IHttpClientFactory创建一个HttpClient对象,之后的操作跟以前一样,但不需要担心其内部资源的释放:

using System.Net.Http;using System.Threading.Tasks;using Microsoft.AspNetCore.Mvc;namespace HttpClientFactoryDemo.Controllers{    [Route("api/[controller]")]    [ApiController]    public class DemoController : ControllerBase    {        IHttpClientFactory _httpClientFactory;        ///         /// 通过构造函数实现注入        ///         ///         public DemoController(IHttpClientFactory httpClientFactory)        {            _httpClientFactory = httpClientFactory;        }        public async Task Get()        {            var client = _httpClientFactory.CreateClient("client_1"); //复用在Startup中定义的client_1的httpclient            var result = await client.GetStringAsync("/page1.html");            var client2 = _httpClientFactory.CreateClient(); //新建一个HttpClient            var result2 = await client.GetAsync("http://www.baidu.com");            return result2.StatusCode.ToString();        }    }}

程序运行结果:

AddHttpClient的源码:

public static IServiceCollection AddHttpClient(this IServiceCollection services){    if (services == null)    {        throw new ArgumentNullException(nameof(services));    }    services.AddLogging();    services.AddOptions();    //    // Core abstractions    //    services.TryAddTransient();    services.TryAddSingleton();    //    // Typed Clients    //    services.TryAdd(ServiceDescriptor.Singleton(typeof(ITypedHttpClientFactory<>), typeof(DefaultTypedHttpClientFactory<>)));    //    // Misc infrastructure    //    services.TryAddEnumerable(ServiceDescriptor.Singleton());    return services;}

看下面这句代码:

services.TryAddSingleton();

这里添加依赖注入的时候为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参数则从上一句的代码中可以看出是以name为键值从一字典中取出,又因为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);}

Options.DefaultName的值为string.Empty。

以上是".NET Core中的HttpClientFactory类怎么用"这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注行业资讯频道!

对象 代码 方法 资源 问题 参数 网络 处理 接口 时间 程序 篇文章 面的 复用 内容 字典 就是 情况 意味 方式 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 重庆办公软件开发系统框架 occi 怎么关闭数据库 江苏网络时间服务器同步 分析软件开发的市场地位 网络安全应急事件管理办法 摩尔庄园汽水商店服务器 服务器内存什么主板都能用吗 创建和修改数据库结构 计算机网络技术考试答案尔雅 学校网络安全知识送同学们 用语句在图书馆数据库创建表 元神俄罗斯在哪个服务器 lua软件开发怎么刷新数据 数据库描述超市模型基本关系 嵌入式软件开发wbs 浙江网络技术代理商 好了互联网科技有限公司 服务器在家可以远程吗 杭州宇博未来网络技术有限公司 中信建投网络安全招聘 阿里云服务器有桌面吗 如何在表里找到我要数据库 农信社软件开发岗考试 网络安全致家长学生一封信 数据库约束的用途 河北什么是网络技术分类标准 打好网络安全的人民战 软件开发的人天单价标准 网络安全的安全教育 重庆石柱农副配送软件开发
0