使用 IHttpClientFactory 实现复原 HTTP 请求

提示

此内容摘自电子书《适用于容器化 .NET 应用程序的 .NET 微服务体系结构》,可在 .NET 文档上获取,也可作为免费可下载的 PDF 脱机阅读。

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

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

.NET 中提供的原始 HttpClient 类的相关问题

常见的原始 HttpClient 类非常易于使用,但在某些情况下,许多开发人员却并未正确使用该类。

虽然此类实现 IDisposable,但在 using 语句中声明和实例化它并非首选操作,因为释放 HttpClient 对象时,基础套接字不会立即释放,这可能会导致套接字耗尽问题。 有关此问题的详细信息,请参阅你正在以错误方式使用 HttpClient,这将导致软件受损的博客文章。

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

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

但是,问题实际上不是 HttpClient 本身,而是 HttpClient 的默认构造函数,因为它创建了一个新的实际 HttpMessageHandler 实例,该实例具有上面提到的“套接字耗尽”和 DNS 更改问题 。

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

替代方法是与配置了 PooledConnectionLifetimeSocketsHttpHandler 结合使用。 此方法适用于长期 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

注意

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

IHttpClientFactory 的多种用法

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

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

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

注意

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

如何结合使用类型化客户端和 IHttpClientFactory

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

下图显示了如何将类型化客户端与 IHttpClientFactory 结合使用:

Diagram showing how typed clients are used with 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() 将 CatalogService、BasketService 和 OrderingService 注册为暂时性服务,使得无需进行其他注册即可直接注入和使用这些服务。

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

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 对象时,都会返回一个新实例。 但每个 HttpClient 使用 IHttpClientFactory 汇集和重用的 HttpMessageHandler,以减少资源消耗,只要 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;
    }
}

Typed Client(在本例中为 CatalogService)由 DI(依赖项注入)激活,这意味着除 HttpClient 外,它还可接受其构造函数中的任何注册服务。

Typed Client 实际上是一个临时对象,这意味着每当需要实例时,就会创建一个新的实例。 它会在每次构造时接收一个新的 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 Client 添加策略和委派处理程序来完成。

其他资源