ASP.NET Core 知多少系列: 总体介绍及目录 http://www.jianshu.com/p/a30fbf5b8a96
本文所讲方式仅适用于托管在 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
来源: https://www.cnblogs.com/sheng-jie/p/9660288.html