使用 IHttpClientFactory 实现可复原 HTTP 请求

提示

此内容摘自电子书《适用于容器化 .NET 应用程序的 .NET 微服务体系结构》,可在 .NET Docs 上找到,或下载免费的 PDF 以便脱机阅读。

适用于容器化 .NET 应用程序的 .NET 微服务体系结构电子书封面缩略图。

IHttpClientFactory 是由自 .NET Core 2.1 起可用的固定工厂 DefaultHttpClientFactory 实现的协定,用于创建在应用程序中使用的 HttpClient 实例。

.NET 中可用的原始 HttpClient 类的问题

可以轻松使用原始和众所周知的 HttpClient 类,但在某些情况下,许多开发人员未正确使用。

虽然该类实现了 IDisposable,但不推荐在 using 语句中声明和实例化它,因为当 HttpClient 对象被释放时,基础套接字不会立即被释放,这可能导致 套接字耗尽 问题。 有关此问题的详细信息,请参阅博客文章 你使用的是 HttpClient 错误,它破坏软件的稳定性。

因此,HttpClient 旨在实例化一次,并在应用程序的整个生命周期中重复使用。 在负载较重的情况下,实例化每个请求的 HttpClient 类将耗尽可用的套接字数。 该问题会导致 SocketException 错误。 解决该问题的可能方法是创建 HttpClient 对象,使其成为单例对象或静态对象,具体说明可以参考这篇 Microsoft 关于 HttpClient 用法的文章。 对于每天运行几次的短生存期控制台应用或类似应用,这可以是一个很好的解决方案。

在长期运行的进程中使用 HttpClient 的共享实例时,开发人员会遇到另一个问题。 如果将 HttpClient 实例化为单例或静态对象,则无法处理 DNS 更改,正如在 dotnet/runtime GitHub 存储库的 问题 中所述。

实际上,问题并不在于 HttpClient 本身,而在于 HttpClient 默认构造函数,因为它创建了一个新的具体实例 HttpMessageHandler,这个实例导致了 套接字耗尽 和上述的 DNS 更改问题。

为了解决上述问题并使 HttpClient 实例可管理,.NET Core 2.1 引入了两种方法,其中一种方法是 IHttpClientFactory。 它是一个接口,用于通过依赖关系注入(DI)在应用中配置和创建 HttpClient 实例。 它还为基于 Polly 的中间件提供扩展,以利用 HttpClient 中的委派处理程序。

替代方法是将 SocketsHttpHandler 与配置的 PooledConnectionLifetime配合使用。 此方法适用于长期 static 实例或 HttpClient 单一实例。 若要详细了解不同的策略,请参阅 .NET的 HttpClient 指南。

Polly 是一个暂时性故障处理库,它通过一些预定义的策略以流畅且线程安全的方式帮助开发人员为其应用程序添加复原能力。

使用 IHttpClientFactory 的好处

同时实现 IHttpMessageHandlerFactoryIHttpClientFactory 当前实现具有以下优势:

  • 提供用于命名和配置逻辑 HttpClient 对象的中心位置。 例如,您可以配置一个客户端(服务代理),使其预先配置为访问特定的微服务。
  • 通过后列方式整理出站中间件的概念:在 HttpClient 中委托处理程序并实现基于 Polly 的中间件以利用 Polly 的复原策略。
  • HttpClient 已经具有委托处理程序的概念,这些委托处理程序可以链接在一起,处理出站 HTTP 请求。 可以将 HTTP 客户端注册到工厂,并且可以使用 Polly 处理程序将 Polly 策略应用于重试、断路器等。
  • 管理 HttpMessageHandler 的生存期,避免在自行管理 HttpClient 生存期时出现上述问题。

提示

由于关联的 HttpMessageHandler 由工厂管理,因此可安全释放由 DI 注入的 HttpClient 实例。 注入的 HttpClient 实例从 DI 的角度来看是暂时性的,而 HttpMessageHandler 实例可以被视为区分范围。 HttpMessageHandler 实例有自己的 DI 范围,独立于应用程序范围(例如,ASP.NET 传入请求范围)。 有关详细信息,请参阅 在 .NET 中使用 HttpClientFactory

注意

IHttpClientFactoryDefaultHttpClientFactory)的实现与 Microsoft.Extensions.DependencyInjection NuGet 包中的 DI 实现紧密关联。 如果需要在没有 DI 或有其他 DI 实现的情况下使用 HttpClient,请考虑使用设置了 PooledConnectionLifetimestatic 或单一实例 HttpClient。 有关详细信息,请参阅 .NET的 HttpClient 指南。

使用 IHttpClientFactory 的多种方式

可通过多种方式在应用程序中使用 IHttpClientFactory

  • 基本用法
  • 使用命名客户端
  • 使用类型化客户端
  • 使用生成的客户端

为了简洁起见,本指南显示了使用 IHttpClientFactory的最结构化方法,即使用类型化客户端(服务代理模式)。 不过,所有选项均已记录,并且当前在此涵盖 IHttpClientFactory 用法的文章中列出。

注意

如果应用需要 Cookie,最好避免在应用中使用 IHttpClientFactory。 有关管理客户端的替代方法,请参阅 有关使用 HTTP 客户端的指南。

如何将类型化客户端与 IHttpClientFactory 配合使用

那么,什么是“类型化客户端”? 它只是为某些特定用途预配置的 HttpClient。 此配置可以包含特定值,例如基本服务器、HTTP 标头或超时。

下图显示了类型化客户端如何与 IHttpClientFactory一起使用:

关系图,显示如何将类型化客户端与 IHttpClientFactory 配合使用。

图 8-4。 结合使用 IHttpClientFactory 和类型化客户端类。

在上图中,ClientService(由控制器或客户端代码使用)使用由注册的 IHttpClientFactory 创建的 HttpClient。 此工厂将池的 HttpMessageHandler 分配给 HttpClient。 当使用扩展方法 AddHttpClient 在 DI 容器中注册 IHttpClientFactory 时,可以使用 Polly 策略配置 HttpClient

若要配置上述结构,请通过安装包含 IServiceCollectionAddHttpClient 扩展方法的 Microsoft.Extensions.Http NuGet 包,在应用程序中添加 IHttpClientFactory。 此扩展方法用于注册内部 DefaultHttpClientFactory 类,后者用作接口 IHttpClientFactory 的单一实例。 它为 HttpMessageHandlerBuilder定义暂时性配置。 此消息处理程序(HttpMessageHandler 对象)获取自池,可供从工厂返回的 HttpClient 使用。

在下一个代码片段中,可以看到如何使用 AddHttpClient() 来注册需要使用 HttpClient的类型化客户端(服务代理)。

// Program.cs
//Add http client services at ConfigureServices(IServiceCollection services)
builder.Services.AddHttpClient<ICatalogService, CatalogService>();
builder.Services.AddHttpClient<IBasketService, BasketService>();
builder.Services.AddHttpClient<IOrderingService, OrderingService>();

按先前片段中所示注册客户端服务,使 DefaultClientFactory 为每个服务创建一个标准 HttpClient。 使用 DI 容器将类型化客户端注册为暂时客户端。 在前面的代码中,AddHttpClient() 注册 CatalogServiceBasketServiceOrderingService 作为暂时性服务,以便直接注入和使用它们,而无需进行其他注册。

还可以在注册中添加特定于实例的配置,例如,配置基址并添加一些复原策略,如下所示:

builder.Services.AddHttpClient<ICatalogService, CatalogService>(client =>
{
    client.BaseAddress = new Uri(builder.Configuration["BaseUrl"]);
})
    .AddPolicyHandler(GetRetryPolicy())
    .AddPolicyHandler(GetCircuitBreakerPolicy());

在下一个示例中,可以看到上述策略之一的配置:

static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
        .WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}

可以在 下一篇文章中找到有关使用 Polly 的更多详细信息。

HttpClient 生存期

每次从 IHttpClientFactory获取 HttpClient 对象时,都会返回一个新实例。 只要 HttpMessageHandler的生命周期未过期,每个 HttpClient 都利用由 IHttpClientFactory 共用和重复使用的 HttpMessageHandler 来减少资源消耗。

处理程序池是可取的,因为每个处理程序通常管理自己的基础 HTTP 连接;创建比必要更多的处理程序可能会导致连接延迟。 某些处理程序还会无限期保持连接打开状态,这可以防止处理程序对 DNS 更改做出反应。

池中的 HttpMessageHandler 对象具有一个生存期,即可以重复使用池中 HttpMessageHandler 实例的时间长度。 默认值为两分钟,但可基于每个类型化客户端重写此值。 要重写该值,请在创建客户端时在返回的 IHttpClientBuilder 上调用 SetHandlerLifetime(),如以下代码所示:

//Set 5 min as the lifetime for the HttpMessageHandler objects in the pool used for the Catalog Typed Client
builder.Services.AddHttpClient<ICatalogService, CatalogService>()
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

每个类型化客户端可以有自己的配置的处理程序生存期值。 将生存期设置为 InfiniteTimeSpan 可禁用处理程序到期。

实现使用注入的和配置的 HttpClient 的类型化客户端类

在上一步中,需要定义类型化客户端类,例如示例代码中的类(如“BasketService”、“CatalogService”、“OrderingService”等)。类型化客户端是一个接受 HttpClient 对象(通过构造函数注入)的类,并使用它调用某些远程 HTTP 服务。 例如:

public class CatalogService : ICatalogService
{
    private readonly HttpClient _httpClient;
    private readonly string _remoteServiceBaseUrl;

    public CatalogService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<Catalog> GetCatalogItems(int page, int take,
                                               int? brand, int? type)
    {
        var uri = API.Catalog.GetAllCatalogItems(_remoteServiceBaseUrl,
                                                 page, take, brand, type);

        var responseString = await _httpClient.GetStringAsync(uri);

        var catalog = JsonConvert.DeserializeObject<Catalog>(responseString);
        return catalog;
    }
}

类型化客户端(示例中CatalogService)由 DI(依赖关系注入)激活,这意味着除了 HttpClient之外,还可以在其构造函数中接受任何已注册的服务。

类型化客户端实际上是一个暂时性对象,这意味着每次需要一个实例时都会创建一个新实例。 它会在每次构造时接收一个新的 HttpClient 实例。 但是,池中的 HttpMessageHandler 对象是被多个 HttpClient 实例重复使用的对象。

使用类型化客户端类

最后,在完成你的类型化类实现后,你可以将它们注册并在 AddHttpClient()中配置。 之后,可以在 DI 注入服务的任何位置使用它们,例如在 Razor 页面代码或 MVC Web 应用控制器中,eShopOnContainers 的以下代码中所示:

namespace Microsoft.eShopOnContainers.WebMVC.Controllers
{
    public class CatalogController : Controller
    {
        private ICatalogService _catalogSvc;

        public CatalogController(ICatalogService catalogSvc) =>
                                                           _catalogSvc = catalogSvc;

        public async Task<IActionResult> Index(int? BrandFilterApplied,
                                               int? TypesFilterApplied,
                                               int? page,
                                               [FromQuery]string errorMsg)
        {
            var itemsPage = 10;
            var catalog = await _catalogSvc.GetCatalogItems(page ?? 0,
                                                            itemsPage,
                                                            BrandFilterApplied,
                                                            TypesFilterApplied);
            //… Additional code
        }

        }
}

至此,上述代码片段仅显示执行常规 HTTP 请求的示例。 但是,以下部分展示了 HttpClient 发出的所有 HTTP 请求如何可以具有弹性策略,例如重试(带有指数退避)、断路器、使用身份验证令牌的安全功能,甚至任何其他自定义功能。 所有这些操作只需将策略和委托处理程序添加到已注册的 Typed 客户端即可完成。

其他资源