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 中断请求了解一下(翻译)的更多相关文章
- ASP.NET Core 中文文档
ASP.NET Core 中文文档 翻译计划 五月中旬 .NET Core RC2 如期发布,我们遂决定翻译 ASP.NET Core 文档.我们在 何镇汐先生. 悲梦先生. 张仁建先生和 雷欧纳德先 ...
- 新的ASP.NET Core 迁移指南
最近在微信里做了一个调查: Web Forms应用程序升级到.NET 6, 收到550份调查,调查还在继续,欢迎参与调查.可以访问链接:https://wj.qq.com/s2/9822949/ac3 ...
- ASP.NET Core文档中Work with Data章节的翻译目录
作为初学者看了相关的教程,遇到的问题有: 1. 教程不是针对初学者,往往在某一方面教的较深,但并不系统,不适合初学者: 2. 虽然翻译的很顺畅,但是谈了自己较多的开发体会,初学者看着困难,尤其是TOM ...
- 【翻译】Asp.net Core介绍
ASP.NET Core is a significant redesign of ASP.NET. This topic introduces the new concepts in ASP.NET ...
- [翻译] ASP.NET Core 2.2 正式版发布
本文为翻译,原文地址:https://blogs.msdn.microsoft.com/webdev/2018/12/04/asp-net-core-2-2-available-today/ 我(文章 ...
- 翻译 Asp.Net Core 2.2.0-preview1已经发布
Asp.Net Core 2.2.0-preview1已经发布 原文地址 ASP.NET Core 2.2.0-preview1 now available 今天我们很高兴地宣布,现在可以试用ASP. ...
- 【翻译】asp.net core中使用MediatR
这篇文章来自:https://ardalis.com/using-mediatr-in-aspnet-core-apps 本文作为翻译,有一些单词翻译成中文可能会有一些误解(对于读者)或者错误(对于作 ...
- [翻译] 初看 ASP.NET Core 3.0 即将到来的变化
[翻译] 初看 ASP.NET Core 3.0 即将到来的变化 原文: A first look at changes coming in ASP.NET Core 3.0 在我们努力完成下一个 m ...
- [翻译] 如何在 ASP.Net Core 中使用 Consul 来存储配置
[翻译] 如何在 ASP.Net Core 中使用 Consul 来存储配置 原文: USING CONSUL FOR STORING THE CONFIGURATION IN ASP.NET COR ...
随机推荐
- SSM-MyBatis-18:Mybatis中二级缓存和第三方Ehcache配置
------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 二级缓存 Mybatis中,默认二级缓存是开启的.可以关闭. 一级缓存开启的.可以被卸载吗?不可以的.一级缓存 ...
- SSM-MyBatis-09:Mybatis中SqlSession的close为什么能造成事务的回滚
------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 经过上几次的查找,笔者我就简单的说一下查找的思路,留给读者自己实践 同样找到sqlsession的实现类,-- ...
- 一个比ack速度快n倍的代码搜索工具: ag
一个比ack速度快n倍的代码搜索工具: ag 银搜索者(The Silver Searcher) 一个类似于代码搜索工具ack,着重于速度. Github: https://github.com ...
- dva/docs/GettingStarted.md
进入目录安装依赖: npm i 或者 yarn install开发: npm run dev npm install 太慢,试试yarn吧.建议用npm install yarn -g进行安装. Co ...
- Spring Boot ConfigurationProperties validate
使用@Query可以在自定义的查询方法上使用@Query来指定该方法要执行的查询语句,比如:@Query("select o from UserModel o where o.uuid=?1 ...
- 电梯调度算法---SCAN算法
请珍惜小编劳动成果,该文章为小编原创,转载请注明出处. 扫描(SCAN)调度算法:总是从磁臂当前位置开始,沿磁臂的移动方向去选择离当前磁臂最近的那个柱面的访问者.如果沿磁臂的方向无请求访问时,就改变磁 ...
- 玩转spring MVC(七)----拦截器
继续在前边的基础上来学习spring MVC中拦截器的使用,下面通过一个例子来实现(完整项目在这里下载:http://download.csdn.net/detail/u012116457/84334 ...
- 连接Redis后执行命令错误 MISCONF Redis is configured to save RDB snapshots
今天在redis中执行setrange name 1 chun 命令时报了如下错误提示: (error) MISCONF Redis is configured to save RDB snapsho ...
- &&和&、||和|的区别
&& 和 || 为短路与 短路或&&若前面的表达式为false,整个逻辑表达式为false,所以后面的表达式无论true和false都无法影响整个表达式的逻辑结果,所以 ...
- ConcurrentLinkedQueue简介
ConcurrentLinkedQueue是一个基于链表的无界线程安全队列,非阻塞实现方式,先进先出,适合高并发的场景. 非阻塞的性能较好,采用CAS,避免加锁的时间,保证数据一致性. 采用" ...