在一个采用依赖注入框架的应用中,我们一般不太推荐利用手工创建的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. mvvm与mvc的定义与区别

    mvvm: 即Model-View-ViewModel(模型-视图-视图模型)的简写. 模型(Model):后端传递的数据 视图(View):即前端渲染的页面 视图模型:是 mvvm 的核心,是连接 ...

  2. Abp 审计模块源码解读

    Abp 审计模块源码解读 Abp 框架为我们自带了审计日志功能,审计日志可以方便地查看每次请求接口所耗的时间,能够帮助我们快速定位到某些性能有问题的接口.除此之外,审计日志信息还包含有每次调用接口时客 ...

  3. Redis sentinel.conf配置文件详解

    redis-sentinel.conf配置项说明如下: 1.port 26379 sentinel监听端口,默认是26379,可以修改. 2.sentinel monitor <master-n ...

  4. sql作业题

    作业题:1.查询选修课程'3-105'且成绩在60到80之间的所有记录.注释:用于指定某个范围使用between and,也可以使用and连接符;答案:法1:select * from sc wher ...

  5. Haproxy LVS Nginx的优缺点总结

    Haproxy  LVS  Nginx的优缺点总结 1.haproxy优点 2.Nginx优点 3.Nginx缺点 4.LVS优点 5.LVS缺点 haproxy优点: haproxy也是支持虚拟主机 ...

  6. 你还不懂java的日志系统吗

    一.背景 在java的开发中,使用最多也绕不过去的一个话题就是日志,在程序中除了业务代码外,使用最多的就是打印日志.经常听到的这样一句话就是"打个日志调试下",没错在日常的开发.调 ...

  7. Docker容器和宿主机互传文件

    1.docker容器向宿主机传送文件 格式: docker cp container_id:<docker容器内的路径> <本地保存文件的路径> 例: docker cp 10 ...

  8. Linux基础入门笔记

    今天带来Linux入门的一些基础的笔记,科班出身的同学们,Linux已经成为了必修课了,下面我带来关于Linux的相关入门知识以及Linux简单的介绍! Linux内核最初只是由芬兰人林纳斯·托瓦兹( ...

  9. mock测试出现Circular view path [trade_records]: would dispatch back to the current handler URL

    这是因为你的Controller中返回的视图名称与你当前的requestMapping名称一样,这并没有很好的解决方案,除非你改掉其中一个名字. 因为springframework test时你并没有 ...

  10. 流程控制( if while )

    目录 流程控制 必知必会 分支结构 if 1.单 if 分支结构 2. if与else连用 3. if, else和 elif if 判断之嵌套 if 练习题 while 循环 while+break ...