Asp.Net Core 轻松学-HttpClient的演进和避坑

楠木大叔       1年前

文/Ron.liang

使用场景

     在 Asp.Net Core 1.0 时代,由于设计上的问题,HttpClient 给开发者带来了无尽的困扰,用 Asp.Net Core 开发团队的话来说就是:我们注意到,HttpClient 被很多开发人员不正确的使用。得益于 .Net Core 不断的版本快速升级;解决方案也一一浮出水面,本文尝试从各个业务场景去剖析 HttpClient的各种使用方式,从而在开发中正确的使用 HttpClient 进行网络请求。


目录


1.0时代发生的事情

     在 1.0 时代,部署在 Linux 上的 Asp.Net Core 应用程序进程出现 “套接字资源耗尽” 的异常,该异常通常是由于不停的创建 HttpClient 的实例后产生,因为每创建一个新的 HttpClient 对象,将会消耗一个套接字资源,而在对象使用完成后,由于设计上的问题,即使手动释放 HttpClient,也极有可能无法释放套接字资源。

     思考下面的代码,在远古时代,下面的代码将会造成 “套接字资源耗尽” 的异常

 public HttpClient CreateHttpClient()
        {
            return new HttpClient();
        }

        // 或者
        public async Task<string> GetData()
        {
            using (var client = new HttpClient())
            {
                var data = await client.GetAsync("https://www.cnblogs.com");
            }

            return null;
        }

     继而引出了下面的使用方法,利用静态对象进行网络请求

private static HttpClient httpClient = null;
        public HttpClient CreateHttpClient()
        {
            if (httpClient == null)
                httpClient = new HttpClient();

            return httpClient;
        }

     上面使用静态对象的方式可以避免"套接字资源耗尽"的异常,但是,有一个致命的问题,当主机DNS更新时,你可能会收到另外一个异常

An error occurred while sending the request. Couldn't resolve host name An error occurred while sending the request. Couldn't resolve host name

     该异常指示无法解析主机名称,其实就是因为静态 HttpClient 对象不会随着主机 DNS 更新而更新,这个时候,你通常需要做的就是:重启服务


正确的使用 HttpClient

     时间来到了.Net Core的2.2时代(其实2.1就可以),官方推荐我们应该使用依赖注入的方式去使用HttpClient,比如在Startup.cs的ConfigureServices方法中加入下面的代码

 public void ConfigureServices(IServiceCollection services)
        {
            ...
            services.AddHttpClient();
        }

     然后再控制器中通过构造方法注入 HttpClient 对象进行使用

public class ValuesController : ControllerBase
    {
        private HttpClient httpClient;
        public ValuesController(HttpClient httpClient)
        {
            this.httpClient = httpClient;
        }

        ...
    }

     在新版本的 Asp.Net Core 中,Asp.Net Core 开发团队引入了 HttpClientFactory。

 public HttpClient CreateHttpClient()
        {
            return HttpClientFactory.Create();
        }

     HttpClientFactory 的主要工作就是创建 HttpClient 对象,但是在创建过程中,通过为每个 HttpClient 对象创建一个单独的清理句柄来对 HttpClient 进行跟踪和管理,以确保在对象使用完成后能够及时的释放网络请求的资源,也就是套接字,具体 HttpClientFactory 内部原理可参考 李志章-DotNetCore深入了解之三HttpClientFactory类.

     更重要的是,HttpClientFactory 内部管理着一个连接句柄池,一旦高并发的到来,HttpClientFactory 内句柄池内使用完成但是未被释放的句柄将被重新使用,虽然使用HttpClientFactory.Create()每次都是返回一个新的 HttpClient 对象,但是其背后的管理句柄是可以复用的,换句话说就是"套接字复用",而且还不会有 DNS 无法同步更新的问题

     所以现在我们明白了为什么要使用 HttpClientFactory 来创建 HttpClient 对象

使用类型化的HttpClient客户端

     在常规应用和微服务的应用场景中,都可以使用类型化的客户端,类型化客户端这个词如果不太好理解,那么你可以理解为为每个业务单独的使用一个HttpClient 客户端,比如获取天气预报,思考下面的代码

public class WeatherService
{
    private HttpClient httpClient;
    public WeatherService(HttpClient httpClient)
    {
        this.httpClient = httpClient;
        this.httpClient.BaseAddress = new Uri("http://www.weather.com.cn");
        this.httpClient.Timeout = TimeSpan.FromSeconds(30);
    }

    public async Task<string> GetData()
    {
        var data = await this.httpClient.GetAsync("/data/sk/101010100.html");
        var result = await data.Content.ReadAsStringAsync();

        return result;
    }
}

     为了在控制器中更好的使用 WeatherService,我们需要把WeatherService 注入到服务中

// 然后,在控制器中使用如下代码

    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private WeatherService weatherService;
        public ValuesController(WeatherService weatherService)
        {
            this.weatherService = weatherService;
        }

        [HttpGet]
        public async Task<ActionResult> Get()
        {
            string result = string.Empty;
            try
            {
                result = await weatherService.GetData();
            }
            catch { }

            return new JsonResult(new { result });
        }
    }

     运行程序,你将得到北京市的天气

{
result: "{"weatherinfo":{"city":"北京","cityid":"101010100","temp":"27.9","WD":"南风","WS":"小于3级","SD":"28%","AP":"1002hPa","njd":"暂无实况","WSE":"<3","time":"17:55","sm":"2.1","isRadar":"1","Radar":"JC_RADAR_AZ9010_JB"}}"
}

     在微服务中,这种做法很常见,而且非常有用,通过使用类型化的客户端,除了在构造方法中注入 HttpClient 外,我们还可以注入任何需要的东西到 WeatherService中,更重要的是,可以对业务应用扩展策略,还方便了管理

     在WeatherService 类型化客户端中,虽然每次都是创建了一个新的 HttpClient 对象,但是其内部的句柄和其它 HttpClient 是共用同一个句柄池,无需担心

对 HttpClient 应用策略

     下面说到的策略组件是业内大名鼎鼎的 Polly(波莉),GitHub 地址:https://github.com/App-vNext/Polly

     使用重试策略,参考 Polly的Wiki 示例代码,使用起来非常简单

首先需要从 NuGet 中引用包Polly Polly.Extensions.Http

     接着在Startup.cs ConfigureServices 方法中应用策略

public void ConfigureServices(IServiceCollection services)
        {
            ...
            services.AddHttpClient<WeatherService>()
                    .SetHandlerLifetime(TimeSpan.FromMinutes(5))
                    .AddPolicyHandler(policy =>
                    {
                        return HttpPolicyExtensions.HandleTransientHttpError()
                                                   .WaitAndRetryAsync(3,
                                                   retryAttempt => TimeSpan.FromSeconds(2),
                                                   (exception, timeSpan, retryCount, context) =>
                                                   {
                                                       Console.ForegroundColor = ConsoleColor.Yellow;
                                                       Console.WriteLine("请求出错了:{0} | {1} ", timeSpan, retryCount);
                                                       Console.ForegroundColor = ConsoleColor.Gray;
                                                   });
                    });
        }

     以上代码表示在请求发生错误的情况下,重试 3 次,每次 2 秒,针对高并发的请求,重试请求间隔建议使用随机值

结语

阅读原文

标签: .net

本文链接:Asp.Net Core 轻松学-HttpClient的演进和避坑

转载请注明文章原始出处 !

0 +1