亲宝软件园·资讯

展开

在ASP.NET Core中用HttpClient(五)——通过CancellationToken取消HTTP请求

码农译站 人气:0

​用户向服务器发送HTTP请求应用程序页面是一种非常可能的情况。当我们的应用程序处理请求时,用户可以从该页面离开。在这种情况下,我们希望取消HTTP请求,因为响应对该用户不再重要。当然,这只是实际应用程序中可能发生的许多情况中的一种,我们希望取消请求。因在本文中,将学习如何使用CancellationToken取消客户端中的HTTP请求。

使用CancellationToken取消使用HttpClient发送的请求

在介绍中,我们指出,如果用户从页面离开,他们就不再需要响应,因此取消该请求是一个很好的做法。但还有更多的原因。HttpClient正在处理异步任务,因此取消一个不再需要的任务将释放我们用来运行任务的线程。这意味着该线程将被返回到一个线程池,在该线程池中,该线程可以用于其他一些工作。这肯定会提高应用程序的可伸缩性。

当然,我们不能就这样取消请求。要执行这样的操作,我们必须使用CancellationTokenSource和CancellationToken。

我们使用CancellationTokenSource来创建CancellationToken,并通知所有CancellationToken的消费者请求已被取消。在我们的例子中,HttpClient将使用CancellationToken并监听通知。一旦收到请求取消通知,我们将使用HttpClient取消该请求。

使用HttpClient实现CancellationToken

我们要做的第一件事是为这个示例创建一个新service:

public class HttpClientCancellationService : IHttpClientServiceImplementation
{
    private static readonly HttpClient _httpClient = new HttpClient();
    private readonly JsonSerializerOptions _options;
​
    public HttpClientCancellationService()
    {
        _httpClient.BaseAddress = new Uri("https://localhost:5001/api/");
        _httpClient.Timeout = new TimeSpan(0, 0, 30);
        _httpClient.DefaultRequestHeaders.Clear();
​
        _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
    }
​
    public async Task Execute()
    {
        throw new NotImplementedException();
    }
}

我们创建了一个HttpClient实例并为其提供配置。同样,对JSON序列化也做同样的事情。在下一篇文章中,我们将学习关于HttpClientFactory的知识,并了解如何将这个配置移动到一个单独的位置,而不会在所有文件中重复它,还将学习如何解决HttpClient可能导致的问题。现在,我们将保持现状。

现在,让我们添加一个新方法来获取所有的公司数据:

private async Task GetCompaniesAndCancel()
{
    using (var response = await _httpClient.GetAsync("companies", HttpCompletionOption.ResponseHeadersRead))
    {
        response.EnsureSuccessStatusCode();
        var stream = await response.Content.ReadAsStreamAsync();
        var companies = await JsonSerializer.DeserializeAsync>(stream, _options);
    }
}

这也是上一篇文章中熟悉的代码,这里不做解释。现在,假设我们想取消这个请求。正如之前说过的,要取消一个请求,我们需要CancellationTokenSource。那么,让我们来实现它:

private async Task GetCompaniesAndCancel()
{
    var cancellationTokenSource = new CancellationTokenSource();
    cancellationTokenSource.CancelAfter(2000);
​
    using (var response = await _httpClient.GetAsync("companies", 
        HttpCompletionOption.ResponseHeadersRead, cancellationTokenSource.Token))
    {
        response.EnsureSuccessStatusCode();
​
        var stream = await response.Content.ReadAsStreamAsync();
​
        var companies = await JsonSerializer.DeserializeAsync>(stream, _options);
    }
}

这里,我们创建了一个新的cancellationTokenSource对象。在创建对象之后,我们希望取消请求。这通常是由用户执行的——通过按下取消按钮或离开一个页面,要取消请求,可以使用两个方法:Cancel()和CancelAfter(),前者会立即取消请求。在本例中,我们使用CancelAfter方法并提供两秒作为参数。最后,我们必须通知HttpClient取消操作。为此,我们提供一个取消令牌作为GetAsync的附加参数。

我们现在就可以测试一下。

测试取消请求

在启动应用程序之前,我们需要确保应用程序启动时调用了我们的方法。为此,我们必须修改Execute方法:

public async Task Execute()
{
    await GetCompaniesAndCancel();
}

同时,我们必须在Program类中注册这个服务:

private static void ConfigureServices(IServiceCollection services)
{
    //services.AddScoped HttpClientCrudService>();
    //services.AddScoped HttpClientPatchService>();
    //services.AddScoped HttpClientStreamService>();
    services.AddScoped HttpClientCancellationService>();
}

现在,让我们启动这两个应用程序:

可以看到我们的请求被取消了。

通过共享CancellationToken改进解决方案

目前的实现对于我们的学习示例非常有用。但在实际的应用程序中,我们希望能够通过将令牌传递给所有请求达到取消不同请求的目的。这将允许在需要时取消所有这些请求。此外,我们希望能够从应用程序的不同部分访问这个CancellationTokenSource,例如当用户单击取消按钮或从页面离开时。在这种情况下,我们不想把CancellationTokenSource隐藏在单个方法中。

private static readonly HttpClient _httpClient = new HttpClient();
private readonly JsonSerializerOptions _options;
private readonly CancellationTokenSource _cancellationTokenSource;
​
public HttpClientCancellationService()
{
    _httpClient.BaseAddress = new Uri("https://localhost:5001/api/");
    _httpClient.Timeout = new TimeSpan(0, 0, 30);
    _httpClient.DefaultRequestHeaders.Clear();
​
    _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
​
    _cancellationTokenSource = new CancellationTokenSource();
}

这里,我们创建了一个CancellationTokenSource只读变量,并在构造函数中实例化它。然后,我们要修改Execute方法:

public async Task Execute()
{
    _cancellationTokenSource.CancelAfter(2000);
    await GetCompaniesAndCancel(_cancellationTokenSource.Token);
}

在这个方法中,我们调用CancelAfter方法来指定要取消请求的周期,并将令牌传递给GetCompaniesAndCancel方法。当然,我们还必须修改GetCompaniesAndCancel方法:

private async Task GetCompaniesAndCancel(CancellationToken token)
{
    using (var response = await _httpClient.GetAsync("companies", 
        HttpCompletionOption.ResponseHeadersRead, token))
    {
        response.EnsureSuccessStatusCode();
​
        var stream = await response.Content.ReadAsStreamAsync();
​
        var companies = await JsonSerializer.DeserializeAsync>(stream, _options);
    }
}

此时,我们的方法接受令牌并使用它来侦听取消通知。现在,可以重新启动API和客户端应用。

可以继续看看如何在我们的应用程序中处理这个异常。

处理TaskCanceledException

如果我们想处理应用程序在取消请求后抛出的异常,只需将请求封装在try-catch块中:

private async Task GetCompaniesAndCancel(CancellationToken token)
{
    try
    {
        using (var response = await _httpClient.GetAsync("companies",
        HttpCompletionOption.ResponseHeadersRead, token))
        {
            response.EnsureSuccessStatusCode();
​
            var stream = await response.Content.ReadAsStreamAsync();
​
            var companies = await JsonSerializer.DeserializeAsync>(stream, _options);
        }
    }
    catch (OperationCanceledException ocex)
    {
        Console.WriteLine(ocex.Message);
    }
}

我们看到应用程序抛出了TaskCanceledException,但是因为它继承了OperationCanceledException类,所以我们可以使用这个类来捕获异常。当然,在catch块中,我们可以执行许多操作,但对于本例来说,只记录消息就足够了。现在,让我们启动这两个应用程序并检查结果:

检查响应的状态代码

使用我们现在的实现,如果响应不成功,我们将抛出异常。为了达到100%的准确性,EnsureSuccessStatusCode()方法将执行此操作。但在许多情况下,我们希望根据响应失败的真正原因提示更用户友好的消息。我们可以检查响应的状态码。也就是说,这里我们将使用一种状态代码,并展示如何用更有意义的消息提供更好的用户体验。

对于本例,我们将使用HttpClientStreamService类。让我们在这个类中创建一个新方法:

private async Task GetNonExistentCompany()
{
    var uri = Path.Combine("companies", "F8088E81-7EFA-4E49-F824-08D8C38D155C");
    using (var response = await _httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead))
    {
        response.EnsureSuccessStatusCode();
​
        var stream = await response.Content.ReadAsStreamAsync();
​
        var companies = await JsonSerializer.DeserializeAsync>(stream, _options);
    }
}

现在我们对整个代码已经很熟悉了,这里提供的Id 不存在于我们的数据库中。因此,我们的API应该返回404。在测试它之前,我们必须修改Execute方法:

public async Task Execute()
{
    //await GetCompaniesWithStream();
    //await CreateCompanyWithStream();
    await GetNonExistentCompany();
}

同时,我们必须在Program类中启用这个服务:

private static void ConfigureServices(IServiceCollection services)
{
    //services.AddScoped HttpClientCrudService>();
    //services.AddScoped HttpClientPatchService>();
    services.AddScoped HttpClientStreamService>();
    //services.AddScoped HttpClientCancellationService>();
}

让我们启动两个应用程序并检查结果:

我们确实得到了404响应,但仍然抛出异常。我们可以改变这一点。

使用状态码

对我们的方法做一个小小的修改:

private async Task GetNonExistentCompany()
{
    var uri = Path.Combine("companies", "F8088E81-7EFA-4E49-F824-08D8C38D155C");
    using (var response = await _httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead))
    {
        if(!response.IsSuccessStatusCode)
        {
            if (response.StatusCode.Equals(HttpStatusCode.NotFound))
            {
                Console.WriteLine("The company you are searching for couldn't be found.");
                return;
            }
​
            response.EnsureSuccessStatusCode();
        }
​
        var stream = await response.Content.ReadAsStreamAsync();
​
        var companies = await JsonSerializer.DeserializeAsync>(stream, _options);
    }
}

首先检查响应是否包含带有IsSuccessStatusCode属性的成功状态代码。

如果没有,则显式检查我们想要处理的状态代码,在本例中是NotFound状态代码。在这种情况下,只需向控制台窗口写入一条信息消息。对于所有其他不成功的状态代码,用EnsureSuccessStatusCode方法抛出一个异常。当然,也可以使用其他状态代码来扩展这个条件,但在这种情况下,最好将该逻辑提取到另一个方法中,以使该方法更具可读性。现在,如果我们启动应用程序:

结论

现在,我们知道了如何使用CancellationToken和CancellationTokenSource取消请求,以及如何使用CancellationTokenSource在不同的请求之间共享令牌。此外,我们还知道如何使用响应中的不同状态代码来防止为每个不成功的响应抛出异常。

在下一篇文章中,我们将学习更多关于HttpClientFactory的内容,并看看这种方法的优点是什么。

原文链接:https://code-maze.com/canceling-http-requests-in-asp-net-core-with-cancellationtoken/

加载全部内容

相关教程
猜你喜欢
用户评论