使用 HttpClient 在 .NET 中发出 HTTP 请求

作者:微信公众号:【架构师老卢】
10-13 18:27
210

在构建 .NET 应用程序时,您通常需要通过 HTTP 与外部 API 进行交互。在 .NET 中处理 HTTP 请求的一种简单方法是使用 .它提供了一种方便的抽象,尤其是对处理 JSON 请求和响应的内置支持。HttpClient

但是,使用不当可能会导致常见问题,例如端口耗尽和次优 DNS 行为。HttpClient

以下是您需要了解的关键事项:

  1. 使用时应避免什么HttpClient
  2. 如何使用IHttpClientFactory
  3. 配置类型化客户端
  4. 为什么不应在单一实例服务中使用类型化的 Client 端
  5. 为您的使用案例选择正确的方法

让我们仔细看看。

使用 HttpClient 的常见陷阱

典型的方法是创建新实例,根据需要对其进行配置并发送请求。这似乎很简单 - 会出什么问题呢?HttpClient

但在实践中,实例被设计为长期存在,并在应用程序的整个生命周期内重复使用。每个实例都管理其连接池,这对于维护性能和防止端口耗尽至关重要。如果您的应用程序频繁创建新实例(尤其是在服务器负载较高的情况下),则可能会耗尽可用端口,从而导致在发送请求时出现运行时异常。HttpClient

那么,您应该如何正确管理呢?HttpClient

public class UserService  
{  
    private readonly UserServiceSettings _settings;  
  
    public UserService(IOptions<UserServiceSettings> settings)  
    {  
        _settings = settings.Value;  
    }  
  
    public async Task<User?> GetUserAsync(string username)  
    {  
        var client = new HttpClient();  
  
        client.DefaultRequestHeaders.Add("Authorization", _settings.UserServiceToken);  
        client.DefaultRequestHeaders.Add("User-Agent", _settings.UserAgent);  
        client.BaseAddress = new Uri("https://api.user.com");  
  
        User? user = await client  
            .GetFromJsonAsync<User>($"users/{username}");  
  
        return user;  
    }  
}

使用 IHttpClientFactory 创建 HttpClient 的有效方法

与其手动管理实例的生命周期,不如使用更智能的方法来创建实例。HttpClientIHttpClientFactory

通过调用该方法,您可以获取新实例并使用它来发送 HTTP 请求。CreateClientHttpClient

为什么这种方法更好?

成本高昂的部分是 底层 ,它管理内部连接池。每个都可以重复使用,从而提高性能并避免端口耗尽等问题。HttpClientHttpMessageHandlerHttpMessageHandler

当您使用 时,它会在创建新实例时缓存并重复使用 。IHttpClientFactoryHttpMessageHandlerHttpClient

请务必注意,以这种方式创建的实例是短期的。HttpClient

public class UserService  
{  
    private readonly UserServiceSettings _settings;  
    private readonly IHttpClientFactory _factory;  
  
    public UserService(  
 IOptions<UserServiceSettings> settings,  
 IHttpClientFactory factory)  
    {  
        _settings = settings.Value;  
        _factory = factory;  
    }  
  
    public async Task<User?> GetUserAsync(string username)  
    {  
        var client = _factory.CreateClient();  
  
        client.DefaultRequestHeaders.Add("Authorization", _settings.UserServiceToken);  
        client.DefaultRequestHeaders.Add("User-Agent", _settings.UserAgent);  
        client.BaseAddress = new Uri("https://api.user.com");  
  
        User? user = await client  
            .GetFromJsonAsync<User>($"users/{username}");  
  
        return user;  
    }  
}

最小化 named Client 端的代码重复

虽然 using 解决了许多与手动创建实例相关的问题,但每次使用 .IHttpClientFactoryHttpClientCreateClient

要简化此操作,您可以通过使用该方法并分配唯一名称来配置命名客户端。此方法接受委托,该委托允许您为实例预配置默认设置,从而减少应用程序中的重复代码AddHttpClientHttpClient

services.AddHttpClient("user", (serviceProvider, client) =>  
{  
    var settings = serviceProvider  
        .GetRequiredService<IOptions<UserServiceSettings>>().Value;  
  
    client.DefaultRequestHeaders.Add("Authorization", settings.UserServiceToken);  
    client.DefaultRequestHeaders.Add("User-Agent", settings.UserAgent);  
  
    client.BaseAddress = new Uri("https://api.user.com");  
});

现在的主要区别在于,您可以通过将客户端的名称传递给方法来检索客户端。CreateClient

但是,这种方法使 的使用更加简单:HttpClient

public class UserService  
{  
    private readonly IHttpClientFactory _factory;  
  
    public UserService(IHttpClientFactory factory)  
    {  
        _factory = factory;  
    }  
  
    public async Task<User?> GetUserAsync(string username)  
    {  
        var client = _factory.CreateClient("user");  
  
        User? user = await client  
            .GetFromJsonAsync<User>($"users/{username}");  
  
        return user;  
    }  
}

将命名客户端替换为类型化客户端

命名客户端的一个缺点是每次检索客户端名称时都需要指定客户端名称。

一个更优雅的解决方案是使用类型化的 client。您可以通过调用 method 来配置这些内容,这允许您设置直接使用的服务。AddHttpClient<TClient>HttpClient

在后台,这仍然使用命名客户端,类型名称作为标识符。

此外,此方法还会为服务(如 )注册具有瞬态生存期。UserService

services.AddHttpClient<UserService>((serviceProvider, client) =>  
{  
    var settings = serviceProvider  
        .GetRequiredService<IOptions<UserServiceSettings>>().Value;  
  
    client.DefaultRequestHeaders.Add("Authorization", settings.UserServiceToken);  
    client.DefaultRequestHeaders.Add("User-Agent", settings.UserAgent);  
  
    client.BaseAddress = new Uri("https://api.user.com");  
});

在 中,您可以注入并使用类型化实例,该实例会自动包含所有预定义配置。UserServiceHttpClient

这样就无需与实例交互或手动创建实例。IHttpClientFactoryHttpClient

public class UserService  
{  
    private readonly HttpClient client;  
  
    public UserService(HttpClient client)  
    {  
        _client = client;  
    }  
  
    public async Task<User?> GetUserAsync(string username)  
    {  
        User? user = await client  
            .GetFromJsonAsync<User>($"users/{username}");  
  
        return user;  
    }  
}

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

将类型化客户端注入单一实例服务可能会导致问题。由于类型化客户端注册为瞬态客户端,因此它们将在单一实例服务的整个生命周期内缓存。这会阻止客户端适应 DNS 更改,从而导致潜在的连接问题。

您应该选择哪个选项?

我已经概述了几种使用 的方法,但您应该何时使用每种方法呢?HttpClient

Microsoft 提供了有效使用的最佳实践:HttpClient

  • 使用具有 配置的 static 或 singleton 来解决端口耗尽和 DNS 更改处理问题。HttpClientPooledConnectionLifetime
  • 如果您想要集中配置,请使用 ,请记住,以这种方式创建的客户端是短期的。IHttpClientFactory
  • 如果您想要 的好处以及简单的配置,请使用类型化客户端
相关留言评论
昵称:
邮箱:
阅读排行