ASP.NET Core 中断请求了解一下(翻译)
本文所讲方式仅适用于托管在Kestrel Server中的应用。如果托管在IIS和IIS Express上时,ASP.NET Core Module(ANCM)并不会告诉ASP.NET Core在客户端断开连接时中止请求。但可喜的是,ANCM预计在.NET Core 2.2中会完善这一机制。
1. 引言
假设有一个耗时的Action,在浏览器发出请求返回响应之前,如果刷新了页面,对于浏览器(客户端)来说前一个请求就会被终止。而对于服务端来说,又是怎样呢?前一个请求也会自动终止,还是会继续运行呢?
下面我们通过实例寻求答案。
2. 实例演示
创建一个
SlowRequestController,再定义一个
Get请求,并通过
Task.Delay(10_000)模拟耗时行为。代码如下:
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才能完成显示。
如果我们检查运行日志,我们发现其输出符合预期:
如果在第一次请求返回之前,刷新页面,结果将是怎样呢??
从日志中我们可以看出:刷新后,第一个请求虽然在客户端被取消了,但是服务端仍旧会持续运行。
从而可以说明MVC的默认行为: 即使用户刷新了浏览器会取消原始请求,但MVC对其一无所知,已经被取消的请求还是会在服务端继续运行,而最终的运行结果将会被丢弃。
这样就会造成严重的性能浪费。如果服务端能感知用户中断了请求,并终止运行耗时的任务就好了。
幸好,ASP.NET Core开发团队体贴的考虑了这一点,允许我们通过以下两种方式来获取客户端的请求是否被终止。
- 通过
HttpContex
的RequestAborted
属性: - 通过方法注入
CancellationToken
参数:
if (HttpContext.RequestAborted.IsCancellationRequested) { // can stop working now }
[HttpGet] public async Task<ActionResult> GetHardWork(CancellationToken cancellationToken) { // ... if (cancellationToken.IsCancellationRequested) { // stop! } // ... }
而这两种方式其实是一样的,因为
HttpContext.RequestAborted和
cancellationToken对应的是同一个对象:
if(cancellationToken == HttpContext.RequestAborted) { // this is true! }
下面我们就来以
cancellationToken为例,看看如何感知客户端请求终止并终止服务端服务。
3. 在Action中使用CancellationToken
CancellationToken是由
CancellationTokenSource创建的轻量级对象。当某个
CancellationTokenSource被取消时,它会通知所有的消费者
CancellationToken。
取消时,
CancellationToken的
IsCancellationRequested属性将设置为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
- 学习ASP.NET Core, 怎能不了解请求处理管道[3]: 自定义一个服务器感受一下管道是如何监听、接收和响应请求的
- 学习ASP.NET Core,怎能不了解请求处理管道[1]: 中间件究竟是个什么东西?
- 学习ASP.NET Core,怎能不了解请求处理管道[1]: 中间件究竟是个什么东西?
- 学习ASP.NET Core,怎能不了解请求处理管道[2]: 服务器在管道中的“龙头”地位
- 学习ASP.NET Core, 怎能不了解请求处理管道[4]: 应用的入口——Startup
- 学习ASP.NET Core,怎能不了解请求处理管道[2]: 服务器在管道中的“龙头”地位
- 学习ASP.NET Core, 怎能不了解请求处理管道[6]: 管道是如何随着WebHost的开启被构建出来的?
- ADO.NET .net core2.0添加json文件并转化成类注入控制器使用 简单了解 iTextSharp实现HTML to PDF ASP.NET MVC 中 Autofac依赖注入DI 控制反转IOC 了解一下 C# AutoMapper 了解一下
- 学习ASP.NET Core, 怎能不了解请求处理管道[5]: 中间件注册可以除了可以使用Startup之外,还可以选择StartupFilter
- asp.net core 创建允许跨域请求的api, cors.
- [翻译]了解ASP.NET底层架构(三)
- (翻译)从底层了解ASP.NET体系结构[转]
- [翻译]了解ASP.NET底层架构(七)
- 通过重建Hosting系统理解HTTP请求在ASP.NET Core管道中的处理流程[上]:采用管道处理请求
- 通过重建Hosting系统理解HTTP请求在ASP.NET Core管道中的处理流程[中]:管道如何处理请求
- [翻译] 初看 ASP.NET Core 3.0 即将到来的变化
- 手把手引进门之 ASP.NET Core & Entity Framework Core(官方教程翻译版 版本3.2.5)
- [翻译] ASP.NET Core 简介
- (翻译)从底层了解ASP.NET体系结构 -转
- [翻译]了解ASP.NET底层架构(四)