第十七节:.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";
            }
        }

    }
View Code

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         }
ConfigureServices
  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     }
View Code
 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     }
类型化客户端-UserService

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;
        }

    }
View Code

 

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鹏飞)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 声     明1 : 本人才疏学浅,用郭德纲的话说“我是一个小学生”,如有错误,欢迎讨论,请勿谩骂^_^。
  • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2019-08-05 07:55  Yaopengfei  阅读(1817)  评论(4编辑  收藏  举报