第十七节:.Net Core中新增HttpClientFactory的前世今生
一. 背景
1.前世
提到HttpClient,在传统的.Net版本中简直臭名昭著,因为我们安装官方用法 using(var httpClient = new HttpClient()),当然可以Dispose,但是在高并发的情况下,连接来不及释放,socket被耗尽,然后就会出现一个喜闻乐见的错误:即各种套接字的问题。
Unable to connect to the remote serverSystem.Net.Sockets.SocketException: Only one usage of each socket address (protocol/network address/port) is normally permitted.
PS:当然我们可以通过修改注册表的默认值,来人为的减少超时时间,但可能会引起其他莫名其妙的问题:
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\TcpTimedWaitDelay])
2.我们之前的解决方案
把HttpClient做成全局单例的,通过共享一个实例,减少了套接字的浪费,实际上由于套接字重用而传输快一点。
关于单例的封装,详见这篇: https://www.cnblogs.com/yaopengfei/p/10301779.html
封装成单例的也会有些不灵活的地方:
(1).因为是复用的 HttpClient,那么一些公共的设置就没办法灵活的调整了,如请求头的自定义。
(2).因为 HttpClient 请求每个 url 时,会缓存该url对应的主机 ip,从而会导致 DNS 更新失效(TTL 失效了)。
3.HttpClientFactiory应运而生
HttpClientFactory 是 ASP.NET CORE 2.1 中新增加的功能。顾名思义 HttpClientFactory 就是 HttpClient 的工厂,内部已经帮我们处理好了对 HttpClient 的管理,不需要我们人工进行对象释放,同时,支持自定义请求头,支持 DNS 更新。
分析:
HttpClient 继承自 HttpMessageInvoker,而 HttpMessageInvoker 实质就是HttpClientHandler。HttpClientFactory 创建的 HttpClient,也即是 HttpClientHandler,只是这些个 HttpClient 被放到了“池子”中,工厂每次在 create 的时候会自动判断是新建还是复用。(默认生命周期为 2min)。简单的理解成 Task 和 Thread 的关系。
4. 补充请求方式的说明
其中Post请求有两种,分别是: "application/x-www-form-urlencoded"表单提交的方式 和 "application/json" Json格式提交的方式。
(1). Post的表单提交的格式为:"userName=admin&pwd=123456"。
(2). Post的Json的提交格式为:将实体(类)转换成json字符串。
二. 几种用法
用到的服务器端的方法:
/// <summary> /// 充当服务端接口 /// </summary> public class ServerController : Controller { [HttpGet] public string CheckLogin(string userName, string pwd) { if (userName == "admin" && pwd == "123456") { return "ok"; } else { return "error"; } } [HttpPost] public string Register1(string userName, string pwd) { if (userName == "admin" && pwd == "123456") { return "ok"; } else { return "error"; } } [HttpPost] public string Register2([FromBody]UserInfor model) { if (model.userName == "admin" && model.pwd == "123456") { return "ok"; } else { return "error"; } } }
1. 基本用法
A.步骤
(1).在ConfigService方法中注册服务:services.AddHttpClient();
(2).通过构造函数全局注入IHttpClientFactory对象,或者通过[FromServices]给某个方法注入。
(3).利用SendAsync方法和HttpRequestMessage对象(可以配置请求方式、表头、请求内容)进行各种请求的异步和同步发送
(PS:下面无论哪种用法,这里都通过SendAsync和HttpRequestMessage进行演示)
B.适用场景
以这种方式使用 IHttpClientFactory 适合重构现有应用。 这不会影响 HttpClient 的使用方式。 在当前创建HttpClient 实例的位置, 使用对 CreateClient 的调用替换这些匹配项。
特别补充:利用GetAsync和PostAsync方法来发送请求的方式,详见下面代码。
2. 命名客户端
A.步骤
(1).在ConfigService方法中注册服务:services.AddHttpClient("client1",c=>{ 相关默认配置 });
(2).通过构造函数全局注入IHttpClientFactory对象,或者通过[FromServices]给某个方法注入
(3).创建Client对象的时候指定命名 CreateClient("client1");其它用法都相同了。
B.适用场景
应用需要有许多不同的 HttpClient 用法(每种用法的配置都不同),可以视情况使用命名客户端。可以在 HttpClient 中注册时指定命名 Startup.ConfigureServices 的配置请求地址的公共部分,表头等。
3. 类型化客户端
A.本质
新建一个类,在类里注入HttpClient对象,在构造函数中进行配置,或者在Startup中进行配置,然后把请求相关的业务封装到方法中,外层直接调用方法即可。
B.步骤
(1).新建UserService类,通过构造函数注入HttpClient对象,然后将请求相关的配置封装在一个方法Login中。
(2).在ConfigService方法中注册服务:services.AddHttpClient<UserService>();
(3).通过构造函数全局注入UserService对象,或者通过[FromServices]给某个方法注入。
(4).调用对应的方法即可。
C.适用场景
根据个人喜好选择即可
分享上述全部代码:
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.Configure<CookiePolicyOptions>(options => 4 { 5 // This lambda determines whether user consent for non-essential cookies is needed for a given request. 6 options.CheckConsentNeeded = context => true; 7 options.MinimumSameSitePolicy = SameSiteMode.None; 8 }); 9 10 //下面是注册HttpClient服务 11 //1. 基本用法 12 services.AddHttpClient(); 13 //2. 命名客户端 14 services.AddHttpClient("client1", c => 15 { 16 c.BaseAddress = new Uri("http://localhost:15319/"); 17 c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); 18 c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); 19 }); 20 //3. 类型化客户端 21 services.AddHttpClient<UserService>(); 22 //可以根据喜好在注册服务的时候配置,就不需要在UserService构造函数中配置了 23 //services.AddHttpClient<UserService>(c => 24 //{ 25 // c.BaseAddress = new Uri("http://localhost:15319/"); 26 // c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); 27 // c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); 28 //}); 29 services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); 30 }
1 public class HomeController : Controller 2 { 3 private readonly IHttpClientFactory _clientFactory; 4 public HomeController(IHttpClientFactory clientFactory) 5 { 6 _clientFactory = clientFactory; 7 } 8 9 public async Task<IActionResult> Index([FromServices] UserService userService) 10 { 11 string url1 = "http://localhost:15319/Server/CheckLogin?userName=admin&pwd=123456"; 12 string url2 = "http://localhost:15319/Server/Register1"; 13 string url3 = "http://localhost:15319/Server/Register2"; 14 15 16 string url4 = "Server/CheckLogin?userName=admin&pwd=123456"; 17 18 /*********************************************一.基本用法*********************************************/ 19 20 #region 01-基本用法(Get请求) 21 { 22 var request = new HttpRequestMessage(HttpMethod.Get, url1); 23 request.Headers.Add("Accept", "application/vnd.github.v3+json"); 24 request.Headers.Add("User-Agent", "HttpClientFactory-Sample"); 25 var client = _clientFactory.CreateClient(); 26 var response = await client.SendAsync(request); 27 string result = ""; 28 if (response.IsSuccessStatusCode) 29 { 30 result = await response.Content.ReadAsStringAsync(); 31 } 32 ViewBag.result = result; 33 34 } 35 #endregion 36 37 #region 02-基本用法(Post请求-表单提交) 38 { 39 var request = new HttpRequestMessage(HttpMethod.Post, url2); 40 //表头的处理 41 request.Headers.Add("Accept", "application/vnd.github.v3+json"); 42 request.Headers.Add("User-Agent", "HttpClientFactory-Sample"); 43 //内容的处理 44 request.Content = new StringContent("userName=admin&pwd=123456", Encoding.UTF8, "application/x-www-form-urlencoded"); 45 var client = _clientFactory.CreateClient(); 46 var response = await client.SendAsync(request); 47 string result = ""; 48 if (response.IsSuccessStatusCode) 49 { 50 result = await response.Content.ReadAsStringAsync(); 51 } 52 } 53 #endregion 54 55 #region 03-基本用法(Post请求-JSON格式提交) 56 { 57 var request = new HttpRequestMessage(HttpMethod.Post, url3); 58 //表头的处理 59 request.Headers.Add("Accept", "application/vnd.github.v3+json"); 60 request.Headers.Add("User-Agent", "HttpClientFactory-Sample"); 61 //内容的处理 62 var user = new 63 { 64 userName = "admin", 65 pwd = "123456" 66 }; 67 request.Content = new StringContent(JsonConvert.SerializeObject(user), Encoding.UTF8, "application/json"); 68 var client = _clientFactory.CreateClient(); 69 var response = await client.SendAsync(request); 70 string result = ""; 71 if (response.IsSuccessStatusCode) 72 { 73 result = await response.Content.ReadAsStringAsync(); 74 } 75 } 76 #endregion 77 78 #region 04-补充其他写法 79 { 80 //上面的三个方法都是利用SendAsync方法配合HttpRequestMessage类来发送Get和两种post请求的,所有的参数设置都是基于HttpRequestMessage对象。 81 //在这里再次补充一下直接利用 GetAsync 和 PostAsync 方法直接来发送Get和post请求(直接.Result不异步了) 82 83 //1. Get请求 84 { 85 var client = _clientFactory.CreateClient(); 86 //配置表头 87 client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); 88 client.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); 89 var response = client.GetAsync(url1).Result; 90 string result = ""; 91 if (response.IsSuccessStatusCode) 92 { 93 result = response.Content.ReadAsStringAsync().Result; 94 } 95 } 96 //2. Post请求-表单提交 97 { 98 var client = _clientFactory.CreateClient(); 99 //配置表头 100 client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); 101 client.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); 102 //配置请求内容 103 var content = new StringContent("userName=admin&pwd=123456", Encoding.UTF8, "application/x-www-form-urlencoded"); 104 var response = client.PostAsync(url2, content).Result; 105 string result = ""; 106 if (response.IsSuccessStatusCode) 107 { 108 result = response.Content.ReadAsStringAsync().Result; 109 } 110 111 } 112 113 //3. Post请求-JSON提交 114 { 115 var client = _clientFactory.CreateClient(); 116 //配置表头 117 client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); 118 client.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); 119 //配置请求内容 120 var user = new 121 { 122 userName = "admin", 123 pwd = "123456" 124 }; 125 var content = new StringContent(JsonConvert.SerializeObject(user), Encoding.UTF8, "application/json"); 126 var response = client.PostAsync(url3, content).Result; 127 string result = ""; 128 if (response.IsSuccessStatusCode) 129 { 130 result = response.Content.ReadAsStringAsync().Result; 131 } 132 } 133 134 } 135 #endregion 136 137 138 /*********************************************二.命名客户端*********************************************/ 139 140 #region 命名客户端(Get请求) 141 { 142 var request = new HttpRequestMessage(HttpMethod.Get, url4); 143 //配置调用的名称 144 var client = _clientFactory.CreateClient("client1"); 145 var response = await client.SendAsync(request); 146 string result = ""; 147 if (response.IsSuccessStatusCode) 148 { 149 result = await response.Content.ReadAsStringAsync(); 150 } 151 } 152 #endregion 153 154 /*********************************************三.类型化客户端*********************************************/ 155 156 #region 类型化客户端(Get请求) 157 { 158 string result = await userService.Login(url4); 159 } 160 #endregion 161 162 163 return View(); 164 } 165 }
1 /// <summary> 2 /// 类型化客户端 3 /// </summary> 4 public class UserService 5 { 6 public HttpClient _client; 7 public UserService(HttpClient client) 8 { 9 client.BaseAddress = new Uri("http://localhost:15319/"); 10 client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); 11 client.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); 12 _client = client; 13 } 14 15 public async Task<string> Login(string urlContent) 16 { 17 var response = await _client.GetAsync(urlContent); 18 response.EnsureSuccessStatusCode(); 19 var result = await response.Content.ReadAsStringAsync(); 20 return result; 21 } 22 }
4. 总结
它们之间不存在严格的优先级。 最佳方法取决于应用的约束条件。
三. 与Refit结合
不做深入研究,可参考:
https://www.xcode.me/code/refit-the-automatic-type-safe-rest-library
https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/http-requests?view=aspnetcore-2.1#outgoing-request-middleware
四. 结合框架进行封装
封装思路:将内容和请求方式等一系列操作封装到方法里,如果需要配置表头,建议采用命名客户端的方式在ConfigureService中进行配置, 需要事先注册好服务,然后把实例化好的IHttpClientFactory对象传入到方法里。
封装了三个方法,分别来处理:Get、两种Post请求
需要用到:【Microsoft.Extensions.Http】程序集,Core MVC中已经默认引入了。
代码分享:
1 /// <summary> 2 /// 基于HttpClientFactory的请求封装 3 /// </summary> 4 public class RequestHelp 5 { 6 /// <summary> 7 /// Get请求 8 /// </summary> 9 /// <param name="clientFactory">实例化好的HttpClientFactory对象</param> 10 /// <param name="url">请求地址</param> 11 /// <param name="clientName">注册名称,默认不指定</param> 12 /// <returns></returns> 13 public static string MyGetRequest(IHttpClientFactory clientFactory, string url, string clientName = "") 14 { 15 var request = new HttpRequestMessage(HttpMethod.Get, url); 16 var client = clientName == "" ? clientFactory.CreateClient() : clientFactory.CreateClient(clientName); 17 var response = client.SendAsync(request).Result; 18 var myResult = response.Content.ReadAsStringAsync().Result; 19 return myResult; 20 } 21 22 /// <summary> 23 /// Post请求-表单形式 24 /// </summary> 25 /// <param name="clientFactory">实例化好的HttpClientFactory对象</param> 26 /// <param name="url">请求地址</param> 27 /// <param name="content">请求内容</param> 28 /// <param name="clientName">注册名称,默认不指定</param> 29 /// <returns></returns> 30 public static string MyPostRequest(IHttpClientFactory clientFactory, string url,string content, string clientName = "") 31 { 32 var request = new HttpRequestMessage(HttpMethod.Post, url); 33 //内容的处理 34 request.Content = new StringContent(content, Encoding.UTF8, "application/x-www-form-urlencoded"); 35 var client = clientName == "" ? clientFactory.CreateClient() : clientFactory.CreateClient(clientName); 36 var response = client.SendAsync(request).Result; 37 var myResult = response.Content.ReadAsStringAsync().Result; 38 return myResult; 39 } 40 41 /// <summary> 42 /// Post请求-Json形式 43 /// </summary> 44 /// <param name="clientFactory">实例化好的HttpClientFactory对象</param> 45 /// <param name="url">请求地址</param> 46 /// <param name="content">请求内容</param> 47 /// <param name="clientName">注册名称,默认不指定</param> 48 /// <returns></returns> 49 public static string MyPostRequestJson(IHttpClientFactory clientFactory, string url, object content, string clientName = "") 50 { 51 var request = new HttpRequestMessage(HttpMethod.Post, url); 52 //内容的处理 53 request.Content = new StringContent(JsonConvert.SerializeObject(content), Encoding.UTF8, "application/json"); 54 var client = clientName == "" ? clientFactory.CreateClient() : clientFactory.CreateClient(clientName); 55 var response = client.SendAsync(request).Result; 56 var myResult = response.Content.ReadAsStringAsync().Result; 57 return myResult; 58 } 59 60 }
1 public class HomeController : Controller 2 { 3 private readonly IHttpClientFactory _clientFactory; 4 public HomeController(IHttpClientFactory clientFactory) 5 { 6 _clientFactory = clientFactory; 7 } 8 9 public async Task<IActionResult> Index([FromServices] UserService userService) 10 { 11 string url1 = "http://localhost:15319/Server/CheckLogin?userName=admin&pwd=123456"; 12 string url2 = "http://localhost:15319/Server/Register1"; 13 string url3 = "http://localhost:15319/Server/Register2"; 14 string url4 = "Server/CheckLogin?userName=admin&pwd=123456"; 15 16 17 /*********************************************测试框架封装*********************************************/ 18 19 #region 测试框架封装 20 { 21 var result1 = RequestHelp.MyGetRequest(_clientFactory, url1); 22 var result2 = RequestHelp.MyGetRequest(_clientFactory, url4, "client1"); 23 //Post-表单提交 24 var result3 = RequestHelp.MyPostRequest(_clientFactory, url2, "userName=admin&pwd=123456"); 25 //Post-Json提交 26 var user = new 27 { 28 userName = "admin", 29 pwd = "123456" 30 }; 31 var result4 = RequestHelp.MyPostRequestJson(_clientFactory, url3, user); 32 33 } 34 #endregion 35 36 return View(); 37 } 38 }
五. 如何在客户端调用
前面的介绍都是基于Core MVC来发送请求,然后可以注入 IHttpClientFactory 对象,那么如何在控制台上调用呢? 这是一类通用的问题,看下面代码,获取 IHttpClientFactory 对象,然后后续的Get和Post请求的写法和前面介绍的完全相同,可以根据自己的喜好进行选择。
var serviceProvider = new ServiceCollection().AddHttpClient().BuildServiceProvider(); IHttpClientFactory httpClientFactory = serviceProvider.GetService<IHttpClientFactory>();
分享一个完整的客户端请求写法:
var serviceProvider = new ServiceCollection().AddHttpClient().BuildServiceProvider(); IHttpClientFactory httpClientFactory = serviceProvider.GetService<IHttpClientFactory>(); string url2 = "http://XXX:8055/Home/SendAllMsg"; var client = httpClientFactory.CreateClient(); var content = new StringContent("msg=123", Encoding.UTF8, "application/x-www-form-urlencoded"); var response = client.PostAsync(url2, content).Result; string result = ""; if (response.IsSuccessStatusCode) { result = response.Content.ReadAsStringAsync().Result; }
新增Utils单独注入封装,Web层不再需要注入HttpClient了【20220831】
/// <summary> /// 基于HttpClientFactory的请求封装 /// 依赖【Microsoft.Extensions.DependencyInjection】和 【Microsoft.Extensions.Http】 /// 【System.Text.Json】 /// 其它层可以直接调用,不再需要注入AddHttpClient了,因为下面封装里已Add进去了 /// </summary> public class RequestHelp { /// <summary> /// Get请求 /// </summary> /// <param name="url">请求地址</param> /// <param name="headers">headers内容,可以不填</param> /// <returns></returns> public static string Get(string url, Dictionary<string, string> headers = null) { var serviceProvider = new ServiceCollection().AddHttpClient().BuildServiceProvider(); IHttpClientFactory clientFactory = serviceProvider.GetService<IHttpClientFactory>(); var request = new HttpRequestMessage(HttpMethod.Get, url); //添加Headers内容 foreach (var item in headers) { request.Headers.Add(item.Key, item.Value); } var client = clientFactory.CreateClient(); var response = client.SendAsync(request).Result; var myResult = response.Content.ReadAsStringAsync().Result; return myResult; } /// <summary> /// Post请求-表单形式 /// </summary> /// <param name="url">请求地址</param> /// <param name="content">请求内容,形如:"userName=admin&pwd=123456"</param> /// <param name="headers">headers内容,可以不填</param> /// <returns></returns> public static string Post(string url, string content, Dictionary<string, string> headers = null) { var serviceProvider = new ServiceCollection().AddHttpClient().BuildServiceProvider(); IHttpClientFactory clientFactory = serviceProvider.GetService<IHttpClientFactory>(); var request = new HttpRequestMessage(HttpMethod.Post, url) { //内容的处理 Content = new StringContent(content, Encoding.UTF8, "application/x-www-form-urlencoded") }; //添加Headers内容 foreach (var item in headers) { request.Headers.Add(item.Key, item.Value); } var client = clientFactory.CreateClient(); var response = client.SendAsync(request).Result; var myResult = response.Content.ReadAsStringAsync().Result; return myResult; } /// <summary> /// Post请求-Json形式 /// </summary> /// <param name="url">请求地址</param> /// <param name="content">请求内容, 形如: /// new {userName = "admin", pwd = "123456"} /// </param> /// <param name="headers">headers内容,可以不填</param> /// <returns></returns> public static string PostJson(string url, object content, Dictionary<string, string> headers = null) { var serviceProvider = new ServiceCollection().AddHttpClient().BuildServiceProvider(); IHttpClientFactory clientFactory = serviceProvider.GetService<IHttpClientFactory>(); var request = new HttpRequestMessage(HttpMethod.Post, url) { //内容的处理 Content = new StringContent(JsonSerializer.Serialize(content), Encoding.UTF8, "application/json") }; //添加Headers内容 foreach (var item in headers) { request.Headers.Add(item.Key, item.Value); } var client = clientFactory.CreateClient(); var response = client.SendAsync(request).Result; var myResult = response.Content.ReadAsStringAsync().Result; return myResult; } }
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 本人才疏学浅,用郭德纲的话说“我是一个小学生”,如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。