一、前言

在我们开发当中经常需要向特定URL地址发送Http请求操作,在.net core 中对httpClient使用不当会造成灾难性的问题,这篇文章主要来分享.net core中通过IHttpClientFactory 工厂来使用HttpClient的正确打开方式。

二、HttpClient使用中的那些坑

2.1 错误使用

using(var client = new HttpClient())

我们可以先来做一个简单的测试,代码如下:

 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();

静态单例模式虽然可以解决上面问题,但是会带来另外一个问题:

  • 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;
} /// <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)。

希望这篇文章对你有帮助,如果对你有帮助请点个推荐,感谢!

.net core HttpClient 使用之掉坑解析(一)的更多相关文章

  1. .net core HttpClient 使用之消息管道解析(二)

    一.前言 前面分享了 .net core HttpClient 使用之掉坑解析(一),今天来分享自定义消息处理HttpMessageHandler和PrimaryHttpMessageHandler ...

  2. .NET Core HttpClient调用腾讯云对象存储Web API的"ERROR_CGI_PARAM_NO_SUCH_OP"问题

    开门见山地说一下问题的原因:调用 web api 时请求头中多了双引号,请求体中少了双引号. 腾讯云提供的对象存储(COS)C# SDK 是基于 .NET Framework 用 WebRequest ...

  3. Docker从入门到掉坑(三):容器太多,操作好麻烦

    前边的两篇文章里面,我们讲解了基于docker来部署基础的SpringBoot容器,如果阅读本文之前没有相关基础的话,可以回看之前的教程. Docker 从入门到掉坑 Docker从入门到掉坑(二): ...

  4. 开发掉坑(二)前端静态资源 Uncaught SyntaxError: Unexpected token <

    某天,有同学反馈后台管理系统出现静态资源无法加载的问题. 复现如下: 进入首页. 点击侧边栏某个子功能,静态资源可正常访问到. 等待10分钟左右,点击侧边栏其他子功能,无法访问到静态资源. 查看控制台 ...

  5. Gopher必读:HttpClient的两个坑位

    http是我们最常见的客户端/服务端传输协议,在golang中,默认的net/http包有一些坑位,需要调整以获得更加性能. 在golang程序中,我也遇到因为不合理使用 http client导致的 ...

  6. AlvinZH掉坑系列讲解(背包DP大作战H~M)

    本文由AlvinZH所写,欢迎学习引用,如有错误或更优化方法,欢迎讨论,联系方式QQ:1329284394. 前言 动态规划(Dynamic Programming),是一个神奇的东西.DP只能意会, ...

  7. Docker 从入门到掉坑

    Docker 介绍 简单的对docker进行介绍,可以把它理解为一个应用程序执行的容器.但是docker本身和虚拟机还是有较为明显的出入的.我大致归纳了一下,可以总结为以下几点: docker自身也有 ...

  8. Docker从入门到掉坑(二):基于Docker构建SpringBoot微服务

    本篇为Docker从入门到掉坑第二篇:基于Docker构建SpringBoot微服务,没有看过上一篇的最好读过 Docker 从入门到掉坑 之后,阅读本篇. 在之前的文章里面介绍了如何基于docker ...

  9. Docker从入门到掉坑(四):上手k8s避坑指南

    在之前的几篇文章中,主要还是讲解了关于简单的docker容器该如何进行管理和操作,在接下来的这篇文章开始,我们将开始进入对于k8s模块的学习 不熟悉的可以先回顾之前的章节,Docker教程系列文章将归 ...

随机推荐

  1. Daily Scrum 1/7/2015

    Process: Zhaoyang: Do some code intergration and test the total feature in the IOS APP. Yandong: Cod ...

  2. 小L的直线

    小学时期的小L发现自己很有艺术细胞,于是买了一块画板,但是他的绘画水平使得他只能连接两点画出一条线段.有一天他决定在一张有n个点的图上作画,即他可以把这n个点任意连接.大家认为平行线是非常不美观的,于 ...

  3. Android内存优化—dumpsys meminfo详解

    原创置顶 不死鸟JGC 最后发布于2018-12-24 14:19:28 阅读数 3960 收藏展开dumpsys 介绍Dumpsys用户系统诊断,它运行在设备上,并提供系统服务状态信息 命令格式: ...

  4. Upload-Labs 实验操作记录

    0x01 安装 下载:https://github.com/c0ny1/upload-labs 环境:简单搭建phpstudy环境即可,记得在upload-labs根目录下创建该文件夹 0x02 文件 ...

  5. fasttext的使用,预料格式,调用方法

    数据格式:分词后的句子+\t__label__+标签 fasttext_model.py from fasttext import FastText import numpy as np def ge ...

  6. jquery字符串转数字

    1:parseInt(string) : 这个函数的功能是从string的开头开始解析,返回一个整数,说起来比较笼统,下面来看几个实例,大家就明白了: parseInt("1234blue& ...

  7. sqlilab less15-17

    less15 试了很多符号,页面根本不显示别的信息,猜测为盲注 可是怎么检测闭合? 万能密码登录 最终试出来'闭合 uname=1' or 1=1 # 接下来就要工具跑 less16 同上用万能密码试 ...

  8. Radware:上周五美国大规模DDoS攻击是如何发生的

    10月21日上午,Dyn遭受到拒绝服务(DoS)攻击,造成了托管DNS网络的中断.成千上万的网站因此变得不可访问,其中包括Amazon EC2.当天晚些时候,当攻击者发起第二轮针对Dyn DNS系统的 ...

  9. javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake

    2019独角兽企业重金招聘Python工程师标准>>> 问题 前两天一个学弟在群里面问一个问题: 请问一下用阿里云服务器发送https请求为什么会失败,是需要有些其他什么配置吗? 同 ...

  10. 爱创课堂每日一题第十五题HTTP和HTTPS?

    HTTP协议通常承载于TCP协议之上,在HTTP和TCP之间添加一个安全协议层(SSL或TSL),这个时候,就成了我们常说的HTTPS.默认HTTP的端口号为80,HTTPS的端口号为443. 转载于 ...