优雅通过HttpClientFactory使用HttpClient
原文链接:https://blog.zhuliang.ltd/2019/02/backend/HttpClient-HttpClientFactory-InNetCore.htmlHttpClient 日常使用及坑点: 在C#中,平时我们在使用HttpClient的时候,会将HttpClient包裹在using内部进行声明和初始化,如:
【优雅通过HttpClientFactory使用HttpClient】转载请注明出处。
using(var httpClient = new HttpClient())
{
//other codes
}
至于为什么?无外乎是:项目代码中就是这样写的,依葫芦画瓢/别人就是这样用的/在微软官方的ASP.NET教程中也是这么干的。
说的技术范点:当你使用继承了IDisposable接口的对象时,建议在using代码块中声明和初始化,当using代码段执行完成后,会自动释放该对象而不需要手动进行显示Dispose操作。
但这里,HttpClient这个对象有点特殊,虽然继承了IDisposable接口,但它是可以被共享的(或者说可以被复用),且线程安全。从项目经验来看,倒是建议在整个应用的生命周期内,复用HttpClient实例,而不是每次RPC请求的时候就实例化一个。(之前在优化公司一个web项目的时候,也曾经因为HttpClient载过一次坑,后面我会进行简述。)
我们先来用个简单的例子做下测试,看为什么不要每次RPC请求都实例化一个HttpClient:
public class Program
{
static void Main(string[] args)
{
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连接情况:
文章图片
e92bde19-eae0-41a4-83ed-08c2038fdd0f.png
- 虽然项目已经运行结束,但是连接依然存在,状态为" TIME_WAIT"(继续等待看是否还有延迟的包会传输过来。)。
默认在windows下,TIME_WAIT状态将会使系统将会保持该连接 240s。
- 这里也就引出了我上面说的载过的一次坑:在高并发的情况下,连接来不及释放,socket被耗尽,耗尽之后就会出现喜闻乐见的一个错误:
#使用jemter压测复现错误信息:
Unable to connect to the remote serverSystem.Net.Sockets.SocketException: Only one usage of each socket address (protocol/network address/port) is normally permitted.
说白话:就是会出现“各种套接字问题”。(码WCF的童鞋可能更加记忆尤新,问题追根溯源都是换汤不换药。)
熊厂里面能够搜索出来的解决方法,基本都是“减少超时时间”,但人为减少了超时时间会出现各种莫名其妙的错误。且无法避免服务器迟早崩溃的问题。
可以通过注册表进行修改默认值:[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\TcpTimedWaitDelay])那么如何处理这个问题?答案已经在上面说了,“复用HttpClient”即可。如:
public class Program
{
private static readonly HttpClient _client = new HttpClient();
static void Main(string[] args)
{
HttpAsync();
Console.WriteLine("Hello World!");
Console.Read();
}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}");
}
}
}
文章图片
232f7b9e-b9f3-4444-9697-e0108c3b09fe.png
- 可以看到,原先10个连接变成了1个连接。(请不要在意两次示例的目标IP不同---SLB导致的,都是百度的ip)
- 另外,因为复用了HttpClient,每次RPC请求的时候,实际上还节约了创建通道的时间,在性能压测的时候也是很明显的提升。曾经因为这一举动,将web项目的TPS从单台600瞬间提升到了2000+,页面请求时间也从1-3s减少至100-300ms,甚是让测试组小伙伴膜拜(当然也包括了一些业务代码的细调。),但知道个中缘由后,一个小改动带来的项目性能提升。。。会让人上瘾:)
- 至于如何创建一个静态HttpClient进行复用,大家可以按项目实际来,如直接创建一个“全局”静态对象,或者通过各类DI框架来创建均可。
- 因为是复用的HttpClient,那么一些公共的设置就没办法灵活的调整了,如请求头的自定义。
- 因为HttpClient请求每个url时,会缓存该url对应的主机ip,从而会导致DNS更新失效(TTL失效了)
HttpClientFactory优势:
HttpClientFactory 是ASP.NET CORE 2.1中新增加的功能。
- “完美”解决了我多年来遇到的这些坑,可以更加专注于业务代码。
- HttpClientFacotry很高效,可以最大程度上节省系统socket。(“JUST USE IT AND FXXK SHUT UP”:P)
- Factory,顾名思义HttpClientFactory就是HttpClient的工厂,内部已经帮我们处理好了对HttpClient的管理,不需要我们人工进行对象释放,同时,支持自定义请求头,支持DNS更新等等等。
从微软源码分析,HttpClient继承自HttpMessageInvoker,而HttpMessageInvoker实质就是HttpClientHandler。HttpClientFactory使用方法:
HttpClientFactory 创建的HttpClient,也即是HttpClientHandler,只是这些个HttpClient被放到了“池子”中,工厂每次在create的时候会自动判断是新建还是复用。(默认生命周期为2min)
还理解不了的话,可以参考Task和Thread的关系,以前碰到HttpClient这个问题的时候,就一直在想微软什么时候官方出一个HttpClient的Factory,虽然时隔了这么多年直到.NET CORE 2.1才出,但也很是兴奋。
借助ASP.NET CORE MVC,可以很方便的进行HttpClient的使用
- 在Startup.cs中进行注册
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}public IConfiguration Configuration { get;
}// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
//other codesservices.AddHttpClient("client_1",config=>//这里指定的name=client_1,可以方便我们后期服用该实例
{
config.BaseAddress= new Uri("http://client_1.com");
config.DefaultRequestHeaders.Add("header_1","header_1");
});
services.AddHttpClient("client_2",config=>
{
config.BaseAddress= new Uri("http://client_2.com");
config.DefaultRequestHeaders.Add("header_2","header_2");
});
services.AddHttpClient();
//other codes
services.AddMvc().AddFluentValidation();
}
}
- 使用,这里直接以controller为例,其他地方自行DI
public class TestController : ControllerBase
{
private readonly IHttpClientFactory _httpClient;
public TestController(IHttpClientFactory httpClient)
{
_httpClient = httpClient;
}public async Task Test()
{
var client = _httpClient.CreateClient("client_1");
//复用在Startup中定义的client_1的httpclient
var result = await client.GetStringAsync("/page1.html");
var client2 = _httpClient.CreateClient();
//新建一个HttpClient
var result2 = await client.GetStringAsync("http://www.site.com/XXX.html");
return null;
}
}
实战用法2:使用自定义类执行HttpClientFactory请求
- 自定义HttpClientFactory请求类
public class SampleClient
{
public HttpClient Client { get;
private set;
}public SampleClient(HttpClient httpClient)
{
httpClient.BaseAddress = new Uri("https://api.SampleClient.com/");
httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
httpClient.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
Client = httpClient;
}
}
- 在Startup.cs中ConfigureService方法中注册SampleClient,代码如下,
services.AddHttpClient();
- 调用:
public class ValuesController : Controller
{
private readonly SampleClient_sampleClient;
;
public ValuesController(SampleClientsampleClient)
{
_sampleClient = sampleClient;
}[HttpGet]
public async Task Get()
{
string result = await_sampleClient.client.GetStringAsync("/");
return Ok(result);
}
}
实战用法3:完全封装HttpClient可以使用下面方法
- 自定义HttpClientFactory请求类
public interface ISampleClient
{
Task GetData();
}
public class SampleClient : ISampleClient
{
private readonly HttpClient _client;
public SampleClient(HttpClient httpClient)
{
httpClient.BaseAddress = new Uri("https://api.SampleClient.com/");
httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
httpClient.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
_client = httpClient;
}
public async Task GetData()
{
return await _client.GetStringAsync("/");
}
}
- 在Startup.cs中ConfigureService方法中注册SampleClient,代码如下,
services.AddHttpClient();
- 调用:
public class ValuesController : Controller
{
private readonly ISampleClient_sampleClient;
;
public ValuesController(ISampleClientsampleClient)
{
_sampleClient = sampleClient;
}[HttpGet]
public async Task Get()
{
string result = await _sampleClient.GetData();
return Ok(result);
}
}
推荐阅读
- gitlab|gitlab 通过备份还原 admin/runner 500 Internal Server Error
- whlie循环和for循环的应用
- 雅姿雅态知识2【怎么坐?才能坐如芍药|雅姿雅态知识2【怎么坐?才能坐如芍药,尽显女人优雅】
- 如何通过锻炼的方法治疗前列腺肥大
- 通过复盘快速成长(附模板)
- MyBatis|MyBatis Generator配置
- 《优雅的力量》读后感
- 运维|如何限制IP 通过 SSH连接服务器
- 运维|Linux 禁止用户或 IP通过 SSH 登录
- 青椒板书--足球