在一个采用依赖注入框架的应用中,我们一般不太推荐利用手工创建的HttpClient对象来进行HTTP调用,使用的HttpClient对象最好利用注入的IHttpClientFactory工厂来创建。前者引起的问题,以及后者带来的好处,将通过如下这几个演示程序展现出来。IHttpClientFactory类型由“Microsoft.Extensions.Http”这个NuGet包提供,“Microsoft.NET.Sdk.Web”SDK具有该包的默认引用。如果采用“Microsoft.NET.Sdk”这个SDK,需要添加该包的引用。(本篇提供的实例已经汇总到《ASP.NET Core 6框架揭秘-实例演示版》)

[S1201]频繁创建HttpClient对象调用API(源代码

[S1202]以单例方式使用HttpClient(源代码

[S1203]利用IHttpClientFactory工厂创建HttpClient对象(源代码

[S1204]直接注入HttpClient对象(源代码

[S1205]定制HttpClient对象(源代码

[S1206]强类型客户端(源代码

[S1207]基于Polly的失败重试(源代码

[S1201]频繁创建HttpClient对象调用API

HttpClient类型实现了IDisposable接口,如果采用在每次调用时创建新的对象,那么按照我们理解的编程规范,调用结束之后就应该主动调用Dispose方法及时地将其释放。如下的演示程序就采用了这种编程方式,我们启动了一个ASP.NET应用,它提供了一个返回“Hello World”的终结点。

using System.Diagnostics;

var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
await app.StartAsync(); while (true)
{
using (var httpClient = new HttpClient())
{
try
{
var reply = await httpClient.GetStringAsync("http://localhost:5000");
Debug.Assert(reply == "Hello World!");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}

ASP.NET应用启动之后,我们在一个无限循环中对它发起调用。每次迭代的创建的HttpClient对象会在完成调用之后被释放。当我们的程序运行之后,初始阶段都没有问题。当调用次数累积到一定规模之后,程序会大量地抛出HttpRequestExcetion异常,并提示“Only one usage of each socket address (protocol/network address/port) is normally permitted”。


图1 频繁创建HttpClient导致的异常

[S1202]以单例方式使用HttpClient

这个演示实例表明频繁创建HttpClient对象是不可取的。如果我们需要自行创建HttpClient对象并频繁地使用它们,应该尽可能地复用这个对象。如果将演示程序改写成如下的形式使用单例的HttpClient对象就不会抛出上面这个异常,但是这又会带来一些额外的问题。HttpRequestExcetion异常在前面的实例中为何会出现,后面的实例究竟又有哪些问题,我们将在后面回答这个问题。

using System.Diagnostics;
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
await app.StartAsync(); var httpClient = new HttpClient();
while (true)
{
try
{
var reply = await httpClient.GetStringAsync("http://localhost:5000");
Debug.Assert(reply == "Hello World!");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}

[S1203]利用IHttpClientFactory工厂创建HttpClient对象

引入IHttpClientFactory工厂将会使一切变得简单,我们只需要在需要进行HTTP调用的时候利用这个工厂创建出对应的HttpClient对象就可以了。虽然HttpClient类型实现了IDisposable接口,我们在完成了调用之后根本不需要去调用它的Dispose方法。在下面的演示程序中,我们调用ServiceCollection对象的AddHttpClient扩展方法对IHttpClientFactory工厂进行了注册,并利用构建出来的IServiceProvider对象得到了这个对象。在每次进行HTTP调用的时候,我们利用这个IHttpClientFactory工厂实时地将HttpClient对象创建出来。

using System.Diagnostics;

var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
await app.StartAsync(); var httpClientFactory = new ServiceCollection()
.AddHttpClient()
.BuildServiceProvider()
.GetRequiredService<IHttpClientFactory>(); while (true)
{
try
{
var reply = await httpClientFactory.CreateClient().GetStringAsync("http://localhost:5000");
Debug.Assert(reply == "Hello World!");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}

[S1204]直接注入HttpClient对象

上面介绍的CreateClient扩展方法还注册加针对HttpClient类型的服务,所以HttpClient对象可以直接作为注入的服务来使用。在如下所示的演示程序中,我们直接利用IServiceProvider对象来创提供HttpClient对象,它与上面演示的程序是等效的(S1204)。

using System.Diagnostics;

var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
await app.StartAsync(); var serviceProvider = new ServiceCollection()
.AddHttpClient()
.BuildServiceProvider();
while (true)
{
try
{
var reply = await serviceProvider.GetRequiredService<HttpClient>().GetStringAsync("http://localhost:5000");
Debug.Assert(reply == "Hello World!");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}

[S1205]定制HttpClient对象

调用IServiceCollection接口的AddHttpClient扩展方法进行服务注册的时候可以对HttpClient作相应的定制,比如可以设置超时时间、默认请求报头和网络代理等。如果应用会涉及针对众多不同类型API的调用,调用不同的API可能需要采用不同的设置,比如局域网内部调用就比外部调用需要更小的超时设置。为了解决这个问题,我们对提供的设置赋予一个唯一的名称,在使用的时候针对这个标识提取对应的设置来创建HttpClient对象,为了方便描述,我们将这个唯一标识HttpClient设置的名称就称为HttpClient的名称。在接下来演示的实例中,我们将设置两个HttpClient来调用指向“www.foo.com”和“www.bar.com”这两个域名的API。为此我们需要在host文件中添加了如下的映射关系

127.0.0.1 www.foo.com
127.0.0.1 www.bar.com

在如下所示的演示实例中,我们为ASP.NET应用注册的终结点会返回包含请求的域名和路径。我们调用IServiceCollection接口的AddHttpClient方法注册了两个名称分别为“foo”和“bar”的HttpClient,并对它们的基础地址进行针对性的设置(S1205)。

using System.Diagnostics;

var app = WebApplication.Create(args);
app.Urls.Add("http://0.0.0.0:80");
app.MapGet("/{path}" , (HttpRequest resquest, HttpResponse response) =>response.WriteAsync($"{resquest.Host}{resquest.Path}"));
await app.StartAsync(); var services = new ServiceCollection();
services.AddHttpClient("foo", httpClient => httpClient.BaseAddress = new Uri("http://www.foo.com"));
services.AddHttpClient("bar", httpClient => httpClient.BaseAddress = new Uri("http://www.bar.com"));
var httpClientFactory = services
.BuildServiceProvider()
.GetRequiredService<IHttpClientFactory>(); var reply = await httpClientFactory.CreateClient("foo").GetStringAsync("abc");
Debug.Assert(reply == "www.foo.com/abc");
reply = await httpClientFactory.CreateClient("bar").GetStringAsync("xyz");
Debug.Assert(reply == "www.bar.com/xyz");

我们将HttpClient的注册名称作为参数调用IHttpClientFactory工厂的Create方法得到对应的HttpClient对象。由于基础地址已经设置好了,所以在进行HTTP调用时只需要指定相对地址(“abc”和“xyz”)就可以了。

[S1206]强类型客户端

所谓“强类型客户端”指的针对具体场景自定义的用于调用指定API的类型,强类型客户端直接使用注入的HttpClient进行HTTP调用。对于上一个实例的应用场景,我们就可以定义如下两个客户端类型FooClient和BarClient,并使用它们分别调用指向不同域名的API。如代码片段所示,我们直接在其构造函数中注入了HttpClient对象,并在GetStringAsync方法中使用它来完成最终的HTTP调用。

public class FooClient
{
private readonly HttpClient _httpClient;
public FooClient(HttpClient httpClient) => _httpClient = httpClient;
public Task<string> GetStringAsync(string path) => _httpClient.GetStringAsync(path);
} public class BarClient
{
private readonly HttpClient _httpClient;
public BarClient(HttpClient httpClient) => _httpClient = httpClient;
public Task<string> GetStringAsync(string path) => _httpClient.GetStringAsync(path);
}

由于FooClient和BarClient对使用的HttpClient具有不同的要求,所以我们采用如下的方式调用IServiceCollection接口的AddHttpClient<TClient>针对客户端类型对HttpClient进行针对设置,具体设置的依然是基础地址。由于AddHttpClient<TClient>扩展方法会将作为泛型参数的TClient类型注册为服务,所以我们可以直接利用IServiceProvider对象提取对应的客户端实例(S1206)。

using App;
using System.Diagnostics; var app = WebApplication.Create(args);
app.Urls.Add("http://0.0.0.0:80");
app.MapGet("/{path}", (HttpRequest resquest, HttpResponse response)=> response.WriteAsync($"{resquest.Host}{resquest.Path}"));
await app.StartAsync(); var services = new ServiceCollection();
services.AddHttpClient<FooClient>("foo", httpClient=> httpClient.BaseAddress = new Uri("http://www.foo.com"));
services.AddHttpClient<BarClient>("bar", httpClient=> httpClient.BaseAddress = new Uri("http://www.bar.com"));
var serviceProvider = services.BuildServiceProvider();
var foo = serviceProvider.GetRequiredService<FooClient>();
var bar = serviceProvider.GetRequiredService<BarClient>(); var reply = await foo.GetStringAsync("abc");
Debug.Assert(reply == "www.foo.com/abc");
reply = await bar.GetStringAsync("xyz");
Debug.Assert(reply == "www.bar.com/xyz");

[S1207]基于Polly的失败重试

在任何环境下都不可能确保次HTTP调用都能成功,所以在失败重试是很有必要的。失败重试是要讲究策略的,返回何种响应状态才需要重试?重试多少次?时间间隔多长?一提到策略化自动重试,大多数人会想到Polly这个开源框架,“Microsoft.Extensions.Http.Polly”这个NuGet包提供了IHttpClientFactory工厂和Polly的整合。在添加了这个包引用之后,我们将演示程序做了如下的修改。如代码片段所示,我们注册的终结点接收到的每三个请求只有一个会返回状态码为200的响应,其余两个响应码均为500。如果客户端能够确保失败后至少进行两次重试,那么就能保证客户端调用100%成功(S1207)。

using Polly;
using Polly.Extensions.Http;
using System.Diagnostics; var app = WebApplication.Create(args);
var counter = 0;
app.MapGet("/", (HttpResponse response) => response.StatusCode = counter++ % 3 == 0 ? 200 : 500);
await app.StartAsync(); var services = new ServiceCollection();
services
.AddHttpClient(string.Empty)
.AddPolicyHandler(HttpPolicyExtensions.HandleTransientHttpError().WaitAndRetryAsync(2, _ => TimeSpan.FromSeconds(1)));
var httpClientFactory = services
.BuildServiceProvider()
.GetRequiredService<IHttpClientFactory>(); while (true)
{
var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:5000");
var response = await httpClientFactory.CreateClient().SendAsync(request);
Debug.Assert(response.IsSuccessStatusCode);
}

如上面的代码片段所示,调用AddHttpClient扩展方法注册了一个默认匿名HttpClient(名称采用空字符串)之后,我们接着调用返回的IHttpClientBuilder对象的AddPolicyHandler扩展方法设置了失败重试策略。AddPolicyHandler方法的参数类型为IAsyncPolicy<HttpResponseMessage>的参数,我们利用HttpPolicyExtensions类型的HandleTransientHttpError静态方法创建一个用来处理偶发错误(比如HttpRequestException异常和5XX/408响应)的PolicyBuilder<HttpResponseMessage>对象。我们最终调用该对象的WaitAndRetryAsync方法返回所需的IAsyncPolicy<HttpResponseMessage>对象,并通过参数设置了重试次数(两次)和每次重试时间间隔(1秒)。

在利用代表依赖注入容器的IServiceProvider对象得到IHttpClientFactory之后,我们在一个无限循环中利用它创建的HttpClient对本地承载的API发起调用,虽然服务端每三次调用只有一次是成功的,但是2次重试足以确保最终的调用是成功的,我们提供的调试断言证实了这一点。

ASP.NET Core 6框架揭秘实例演示[17]:利用IHttpClientFactory工厂来创建HttpClient的更多相关文章

  1. ASP.NET Core 6框架揭秘实例演示[07]:文件系统

    ASP.NET Core应用具有很多读取文件的场景,如读取配置文件.静态Web资源文件(如CSS.JavaScript和图片文件等).MVC应用的视图文件,以及直接编译到程序集中的内嵌资源文件.这些文 ...

  2. ASP.NET Core 6框架揭秘实例演示[08]:配置的基本编程模式

    .NET的配置支持多样化的数据源,我们可以采用内存的变量.环境变量.命令行参数.以及各种格式的配置文件作为配置的数据来源.在对配置系统进行系统介绍之前,我们通过几个简单的实例演示一下如何将具有不同来源 ...

  3. ASP.NET Core 6框架揭秘实例演示[09]:配置绑定

    我们倾向于将IConfiguration对象转换成一个具体的对象,以面向对象的方式来使用配置,我们将这个转换过程称为配置绑定.除了将配置树叶子节点配置节的绑定为某种标量对象外,我们还可以直接将一个配置 ...

  4. ASP.NET Core 6框架揭秘实例演示[10]:Options基本编程模式

    依赖注入使我们可以将依赖的功能定义成服务,最终以一种松耦合的形式注入消费该功能的组件或者服务中.除了可以采用依赖注入的形式消费承载某种功能的服务,还可以采用相同的方式消费承载配置数据的Options对 ...

  5. ASP.NET Core 6框架揭秘实例演示[11]:诊断跟踪的几种基本编程方式

    在整个软件开发维护生命周期内,最难的不是如何将软件系统开发出来,而是在系统上线之后及时解决遇到的问题.一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根 ...

  6. ASP.NET Core 6框架揭秘实例演示[12]:诊断跟踪的进阶用法

    一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根据当前的运行状态预知未来可能发生的问题,并将问题扼杀在摇篮中.诊断跟踪能够帮助我们有效地纠错和排错&l ...

  7. ASP.NET Core 6框架揭秘实例演示[13]:日志的基本编程模式[上篇]

    <诊断跟踪的几种基本编程方式>介绍了四种常用的诊断日志框架.其实除了微软提供的这些日志框架,还有很多第三方日志框架可供我们选择,比如Log4Net.NLog和Serilog 等.虽然这些框 ...

  8. ASP.NET Core 6框架揭秘实例演示[14]:日志的进阶用法

    为了对各种日志框架进行整合,微软创建了一个用来提供统一的日志编程模式的日志框架.<日志的基本编程模式>以实例演示的方式介绍了日志的基本编程模式,现在我们来补充几种"进阶" ...

  9. ASP.NET Core 6框架揭秘实例演示[15]:针对控制台的日志输出

    针对控制台的ILogger实现类型为ConsoleLogger,对应的ILoggerProvider实现类型为ConsoleLoggerProvider,这两个类型都定义在 NuGet包"M ...

随机推荐

  1. vector概述

    vector是一个能够支持任何类型的容器,本身为一个可以动态增长的数组. 1.vector基本数据结构 STL中所有的容器都包括三部分: 迭代器,遍历容器的元素,控制容器空间的边界和元素移动. 构造函 ...

  2. Redis下载安装与配置(windows)

    一.Redis下载 Redis官网建议使用Linux进行部署,未提供windows版本的Redis,但微软开发和维护着Windows64版本的Redis. Windows64版本的Redis下载地址: ...

  3. 羽夏看Win系统内核—— APC 篇

    写在前面   此系列是本人一个字一个字码出来的,包括示例和实验截图.由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新. 如有好的建议,欢迎反馈.码字不易, ...

  4. 「JOI 2014 Final」飞天鼠

    「JOI 2014 Final」飞天鼠 显然向上爬是没有必要的,除非会下降到地面以下,才提高到刚好为0. 到达一个点有两种情况:到达高度为0和不为0. 对于高度不为0的情况,显然花费的时间越少高度越高 ...

  5. Atcoder ARC-061

    ARC061(2020.7.10) A 暴力 \(dfs\) 即可. B 考虑统计以每个点为矩阵中心的答案,显然一个黑点只会影响周围九个黑点,使用 \(map\) 来记录这个值,每次修改修改一下答案数 ...

  6. 电脑预装Office2016打开Word时点击保存弹出“word无法启动转换器RECOVR32.CNV”对话框问题的修复方法

    感谢大佬:https://blog.csdn.net/qq_41969790/article/details/85161701 1.问题描述:电脑预装的Office2016,家庭和学生版正版.每次打开 ...

  7. 20161206日更新CocoaPods版本

    从网上下载的工程第三方库需要更新,但当我执行pod update时提示以下错误: [!] The `master` repo requires CocoaPods 1.0.0 -  (currentl ...

  8. JAVA 对象的创建与克隆

    目录 一.对象的4种创建方式 二.通过new创建对象 三.反射 四.克隆对象 浅拷贝 深拷贝 五.反序列化 六.补充 一.对象的4种创建方式 new 创建 反射 克隆 反序列化 二.通过new创建对象 ...

  9. 我们一起来学grep

    文章目录 grep 介绍 grep 命令格式 grep 命令选项 grep 实例 查找指定进程 查找指定进程个数 从文件中读取关键词进行搜索 从多个文件中查找关键字 输出以u开头的行 输出非u开头的行 ...

  10. 攻防世界--神奇的Modbus

    使用Wireshark在数据包中寻找flag flag是明文形式存储wireshark输入modbus过滤并追踪tcp流追踪tcp流 得到结果:ctf{Easy_Modbus} (虽然在wiresha ...