Skip to content
Go back

在 .NET 中正确使用 HttpClient 的方式

摘要

原文 The Right Way To Use HttpClient In .NET


如果您正在构建一个 .NET 应用程序,那么您很可能需要通过 HTTP 调用一个 外部 API。

在 .NET 中发送 HTTP 请求的简便方法是使用 HttpClient。它是一个很棒的抽象工具,尤其是支持 JSON 载荷和响应的方法。

不幸的是,很容易误用 HttpClient。

端口耗尽 和 DNS 行为 是一些最常见的问题。

因此,这里有一些关于使用 HttpClient 需要知道的事情:

让我们深入了解!

使用 HttpClient 的初学者方式

使用 HttpClient 的最简单方法就是创建一个新实例,设置所需的属性,并使用它来发送请求。

可能会出现什么问题呢?

HttpClient 实例应该是 长期存活的,并且在应用程序的生命周期内重复使用。

每个实例都使用自己的 连接池 出于隔离目的,但也是为了防止 端口耗尽。如果服务器负载很高,而您的应用程序不断地创建新连接,可能会导致可用端口耗尽。在尝试发送请求时,这将在运行时导致异常。

那么,如何避免这个问题呢?

public class GitHubService
{
    private readonly GitHubSettings _settings;

    public GitHubService(IOptions<GitHubSettings> settings)
    {
        _settings = settings.Value;
    }

    public async Task<GitHubUser?> GetUserAsync(string username)
    {
        var client = new HttpClient();

        client.DefaultRequestHeaders.Add("Authorization", _settings.GitHubToken);
        client.DefaultRequestHeaders.Add("User-Agent", _settings.UserAgent);
        client.BaseAddress = new Uri("https://api.github.com");

        GitHubUser? user = await client
            .GetFromJsonAsync<GitHubUser>($"users/{username}");

        return user;
    }
}

使用 IHttpClientFactory 智能创建 HttpClient

您可以使用 IHttpClientFactory 来创建 HttpClient 实例,而不用自己管理 HttpClient 的生命周期。

只需调用 CreateClient 方法,并使用返回的 HttpClient 实例来发送您的 HTTP 请求。

为什么这是一个更好的方法呢?

HttpClient 的开销大的部分是实际的消息处理器 - HttpMessageHandler。每个 HttpMessageHandler 都有一个内部的 HTTP 连接池,可以重用。

IHttpClientFactory 将 缓存 HttpMessageHandler 并在创建新的 HttpClient 实例时重用它。

这里的一个重要说明是通过 IHttpClientFactory 创建的 HttpClient 实例是意味着 短暂存活 的。

public class GitHubService
{
    private readonly GitHubSettings _settings;
    private readonly IHttpClientFactory _factory;

    public GitHubService(
        IOptions<GitHubSettings> settings,
        IHttpClientFactory factory)
    {
        _settings = settings.Value;
        _factory = factory;
    }

    public async Task<GitHubUser?> GetUserAsync(string username)
    {
        var client = _factory.CreateClient();

        client.DefaultRequestHeaders.Add("Authorization", _settings.GitHubToken);
        client.DefaultRequestHeaders.Add("User-Agent", _settings.UserAgent);
        client.BaseAddress = new Uri("https://api.github.com");

        GitHubUser? user = await client
            .GetFromJsonAsync<GitHubUser>($"users/{username}");

        return user;
    }
}

通过命名客户端减少代码重复

使用 IHttpClientFactory 可以解决手动创建 HttpClient 的大部分问题。然而,我们仍然需要在每次从 CreateClient 方法获取新的 HttpClient 时配置默认的请求参数。

你可以通过调用 AddHttpClient 方法,并传递所需名称来配置一个 命名客户端。AddHttpClient 接受一个委托,您可以使用它来配置 HttpClient 实例上的默认参数。

services.AddHttpClient("github", (serviceProvider, client) =>
{
    var settings = serviceProvider
        .GetRequiredService<IOptions<GitHubSettings>>().Value;

    client.DefaultRequestHeaders.Add("Authorization", settings.GitHubToken);
    client.DefaultRequestHeaders.Add("User-Agent", settings.UserAgent);

    client.BaseAddress = new Uri("https://api.github.com");
});

主要区别在于,您现在需要通过传递客户端的名称来获取客户端。

但使用 HttpClient 看起来简单多了:

public class GitHubService
{
    private readonly IHttpClientFactory _factory;

    public GitHubService(IHttpClientFactory factory)
    {
        _factory = factory;
    }

    public async Task<GitHubUser?> GetUserAsync(string username)
    {
        var client = _factory.CreateClient("github");

        GitHubUser? user = await client
            .GetFromJsonAsync<GitHubUser>($"users/{username}");

        return user;
    }
}

用类型化客户端取代命名客户端

使用 命名客户端 的缺点是每次都需要通过传递名称来解析一个 HttpClient。

有一个更好的方法可以通过配置一个 类型化客户端 来实现相同的行为。你可以通过调用 AddClient<TClient> 方法,并配置将使用 HttpClient 的服务来做到这一点。

在底层,这仍然是使用一个 命名客户端,其中名称与类型名称相同。

这也会注册 GitHubService 为 瞬态生命周期。

services.AddHttpClient<GitHubService>((serviceProvider, client) =>
{
    var settings = serviceProvider
        .GetRequiredService<IOptions<GitHubSettings>>().Value;

    client.DefaultRequestHeaders.Add("Authorization", settings.GitHubToken);
    client.DefaultRequestHeaders.Add("User-Agent", settings.UserAgent);

    client.BaseAddress = new Uri("https://api.github.com");
});

在 GitHubService 内,您注入并使用已应用所有配置的类型化 HttpClient 实例。

不再需要处理 IHttpClientFactory 和手动创建 HttpClient 实例。

public class GitHubService
{
    private readonly HttpClient client;

    public GitHubService(HttpClient client)
    {
        _client = client;
    }

    public async Task<GitHubUser?> GetUserAsync(string username)
    {
        GitHubUser? user = await client
            .GetFromJsonAsync<GitHubUser>($"users/{username}");

        return user;
    }
}

为什么应该避免在单例服务中使用类型化客户端

如果您将 类型化客户端 注入到一个 单例服务 中,可能会遇到一个 问题。由于 类型化客户端 是 瞬态的,将其注入 单例服务 会导致它在 单例服务 的生命周期内被缓存。

这将阻止 类型化客户端 响应 DNS 变化。

如果您想在 单例服务 中使用 类型化客户端,推荐的方法是使用 SocketsHttpHandler 作为主处理器,并配置 PooledConnectionLifetime。

由于 SocketsHttpHandler 将处理连接池,您可以通过将 HandlerLifetime 设置为 Timeout.InfiniteTimeSpan 来禁用在 IHttpClientFactory 级别的循环。

services.AddHttpClient<GitHubService>((serviceProvider, client) =>
{
    var settings = serviceProvider
        .GetRequiredService<IOptions<GitHubSettings>>().Value;

    client.DefaultRequestHeaders.Add("Authorization", settings.GitHubToken);
    client.DefaultRequestHeaders.Add("User-Agent", settings.UserAgent);

    client.BaseAddress = new Uri("https://api.github.com");
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
    return new SocketsHttpHandler()
    {
        PooledConnectionLifetime = TimeSpan.FromMinutes(15)
    };
})
.SetHandlerLifetime(Timeout.InfiniteTimeSpan);

应该在什么时候使用哪个选项?

我向您展示了使用 HttpClient 的几个可能的选项。

但您应该在什么时候使用哪个呢?

微软很好地为我们提供了一套最佳实践和 HttpClient 的推荐使用指南。

我更喜欢使用 类型化客户端,并且我知道它被配置为 瞬态服务。



Tags


Previous

SignalR 简化:使用 SimpleR 为 ASP.NET Core 创建高效的纯 WebSocket 服务器

Next

在 ASP.NET Core 中通过 Delegating Handlers 扩展 HttpClient