ASP.NET Core 中断请求了解一下(翻译)

ASP.NET Core知多少系列:整体介绍及目录api

本文所讲方式仅适用于托管在Kestrel Server中的应用。若是托管在IIS和IIS Express上时,ASP.NET Core Module(ANCM)并不会告诉ASP.NET Core在客户端断开链接时停止请求。但可喜的是,ANCM预计在.NET Core 2.2中会完善这一机制。浏览器

1. 引言

假设有一个耗时的Action,在浏览器发出请求返回响应以前,若是刷新了页面,对于浏览器(客户端)来讲前一个请求就会被终止。而对于服务端来讲,又是怎样呢?前一个请求也会自动终止,仍是会继续运行呢?mvc

下面咱们经过实例寻求答案。async

2. 实例演示

建立一个SlowRequestController,再定义一个Get请求,并经过Task.Delay(10_000)模拟耗时行为。代码以下:ide

public class SlowRequestController : Controller
{
    private readonly ILogger _logger;

    public SlowRequestController(ILogger<SlowRequestController> logger)
    {
        _logger = logger;
    }

    [HttpGet("/slowtest")]
    public async Task<string> Get()
    {
        _logger.LogInformation("Starting to do slow work");

        // slow async action, e.g. call external api
        await Task.Delay(10_000);

        var message = "Finished slow delay of 10 seconds.";

        _logger.LogInformation(message);

        return message;
    }
}

若是咱们发起请求,那么该页面将耗时10s才能完成显示。
页面展现
若是咱们检查运行日志,咱们发现其输出符合预期:
运行Log性能

若是在第一次请求返回以前,刷新页面,结果将是怎样呢??测试

刷新后运行日志
从日志中咱们能够看出:刷新后,第一个请求虽然在客户端被取消了,可是服务端仍旧会持续运行。this

从而能够说明MVC的默认行为: 即便用户刷新了浏览器会取消原始请求,但MVC对其一无所知,已经被取消的请求仍是会在服务端继续运行,而最终的运行结果将会被丢弃。.net

这样就会形成严重的性能浪费。若是服务端能感知用户中断了请求,并终止运行耗时的任务就行了。日志

幸亏,ASP.NET Core开发团队体贴的考虑了这一点,容许咱们经过如下两种方式来获取客户端的请求是否被终止。

  1. 经过HttpContexRequestAborted属性:
  2. 经过方法注入CancellationToken参数:
if (HttpContext.RequestAborted.IsCancellationRequested)
{
    // can stop working now
}
[HttpGet]
public async Task<ActionResult> GetHardWork(CancellationToken cancellationToken)
{
    // ...
 
    if (cancellationToken.IsCancellationRequested)
    {
        // stop!
    }
     
    // ...
}

而这两种方式实际上是同样的,由于HttpContext.RequestAbortedcancellationToken对应的是同一个对象:

if(cancellationToken == HttpContext.RequestAborted)
{
    // this is true!
}

下面咱们就来以cancellationToken为例,看看如何感知客户端请求终止并终止服务端服务。

3. 在Action中使用CancellationToken

CancellationToken是由CancellationTokenSource建立的轻量级对象。当某个CancellationTokenSource被取消时,它会通知全部的消费者CancellationToken

取消时,CancellationTokenIsCancellationRequested属性将设置为True,表示CancellationTokenSource已取消。

再回到前面的实例,咱们有一个长期运行的操做方法(例如,经过调用许多其余API生成只读报告)。因为它是一种昂贵的方法,咱们但愿在用户取消请求时尽快中止执行操做。

下面的代码显示了经过在action方法中注入一个CancellationToken,并将其传递给Task.Delay,来达到同步终止服务端请求的目的:

public class SlowRequestController : Controller
{
    private readonly ILogger _logger;

    public SlowRequestController(ILogger<SlowRequestController> logger)
    {
        _logger = logger;
    }

    [HttpGet("/slowtest")]
    public async Task<string> Get(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Starting to do slow work");

        // slow async action, e.g. call external api
        await Task.Delay(10_000, cancellationToken);

        var message = "Finished slow delay of 10 seconds.";

        _logger.LogInformation(message);

        return message;
    }
}

MVC将使用CancellationTokenModelBinder自动将Action中的任何CancellationToken参数绑定到HttpContext.RequestAborted。当咱们在Startup.ConfigureServices()中调用services.AddMvc()services.AddMvcCore()时,CancellationTokenModelBinder模型绑定器就会被自动注册。

经过这个小改动,咱们再尝试在第一个请求返回以前刷新页面,从日志中咱们发现,第一个请求将不会继续完成。而是当Task.Delay检测到CancellationToken.IsCancellationRequested属性为true时当即中止执行时并抛出TaskCancelledException
运行日志

简而言之,用户刷新浏览器,在服务端经过抛出TaskCancelledException异常终止了第一个请求,而该异常经过请求管道再传播回来。

在这个场景中,Task.Delay()会监视CancellationToken,所以无需咱们手动检查CancellationToken是否被取消。

4. 手动检查CancellationToken状态

若是你正在调用支持CancellationToken的内置方法,好比Task.Delay()HttpClient.SendAsync(),那么你能够直接传入CancellationToken,并让内部方法负责实际取消。
在其余状况下,您可能正在进行一些同步工做,您但愿可以取消这些工做。例如,假设正在构建一份报告来计算公司员工的全部佣金。你循环每一个员工,而后遍历他们的每一笔销售。

可以在中途取消此报告生成的简单解决方案是检查for循环内的CancellationToken,若是用户取消请求则跳出循环。
如下示例经过循环10次并执行某些同步(不可取消)工做来表示此类状况,该工做由对Thread.Sleep()来模拟。在每一个循环开始时,咱们检查CancellationToken,若是取消则抛出异常。这使得咱们能够终止一个长时间运行的同步任务。

public class SlowRequestController : Controller
{
    private readonly ILogger _logger;

    public SlowRequestController(ILogger<SlowRequestController> logger)
    {
        _logger = logger;
    }

    [HttpGet("/slowtest")]
    public async Task<string> Get(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Starting to do slow work");

        for(var i=0; i<10; i++)
        {
            cancellationToken.ThrowIfCancellationRequested();
            // slow non-cancellable work
            Thread.Sleep(1000);
        }
        var message = "Finished slow delay of 10 seconds.";

        _logger.LogInformation(message);

        return message;
    }
}

如今,若是你取消请求,则对ThrowIfCancelletionRequested()的调用将抛出一个OperationCanceledException,它将再次传播回过滤器管道和中间件管道。

5. 使用ExceptionFilter捕捉取消异常

ExceptionFilters是一个MVC概念,可用于处理在您的操做方法或操做过滤器中发生的异常。能够参考官方文档

能够将过滤器应用到控制器级别和操做级别,也能够应用于全局级别。为了简单起见,咱们建立一个过滤器并添加到全局过滤器。

public class OperationCancelledExceptionFilter : ExceptionFilterAttribute
{
    private readonly ILogger _logger;

    public OperationCancelledExceptionFilter(ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<OperationCancelledExceptionFilter>();
    }
    public override void OnException(ExceptionContext context)
    {
        if(context.Exception is OperationCanceledException)
        {
            _logger.LogInformation("Request was cancelled");
            context.ExceptionHandled = true;
            context.Result = new StatusCodeResult(499);
        }
    }
}

咱们经过重载OnException方法并特殊处理OperationCanceledException异常便可成功捕获取消异常。

Task.Delay()抛出的异常是TaskCancelledException类型,其为OperationCanceledException的基类,因此,以上过滤器也可正确捕捉。

而后注册过滤器:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc(options =>
        {
            options.Filters.Add<OperationCancelledExceptionFilter>();
        });
    }
}

如今再测试,咱们发现运行日志将不会包含异常信息,取而代之的是咱们自定义的信息。

6. 最后

经过本文,咱们知道用户能够经过点击浏览器上的中止或从新加载按钮随时取消Web应用的请求。而实际上仅仅是终止了客户端的请求,服务端的请求还在继续运行。对于简单耗时短的请求来讲,咱们能够不予理睬。可是,对于耗时任务来讲,咱们却不能够置若罔闻,由于其有很高的性能损耗。

而如何解决呢?其关键是经过CancellationToken来捕捉用户请求的状态,从而根据须要进行相应的处理。

参考资料:
CancellationTokens and Aborted ASP.NET Core Requests
Using CancellationTokens in ASP.NET Core MVC controllers

相关文章
相关标签/搜索