本文所講方式僅適用於託管在Kestrel Server中的應用。若是託管在IIS和IIS Express上時,ASP.NET Core Module(ANCM)並不會告訴ASP.NET Core在客戶端斷開鏈接時停止請求。但可喜的是,ANCM預計在.NET Core 2.2中會完善這一機制。瀏覽器
假設有一個耗時的Action,在瀏覽器發出請求返回響應以前,若是刷新了頁面,對於瀏覽器(客戶端)來講前一個請求就會被終止。而對於服務端來講,又是怎樣呢?前一個請求也會自動終止,仍是會繼續運行呢?mvc
下面咱們經過實例尋求答案。async
建立一個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才能完成顯示。
若是咱們檢查運行日誌,咱們發現其輸出符合預期:
性能
若是在第一次請求返回以前,刷新頁面,結果將是怎樣呢??測試
從日誌中咱們能夠看出:刷新後,第一個請求雖然在客戶端被取消了,可是服務端仍舊會持續運行。this
從而能夠說明MVC的默認行爲: 即便用戶刷新了瀏覽器會取消原始請求,但MVC對其一無所知,已經被取消的請求仍是會在服務端繼續運行,而最終的運行結果將會被丟棄。.net
這樣就會形成嚴重的性能浪費。若是服務端能感知用戶中斷了請求,並終止運行耗時的任務就行了。日誌
幸虧,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
爲例,看看如何感知客戶端請求終止並終止服務端服務。
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
是否被取消。
若是你正在調用支持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
,它將再次傳播回過濾器管道和中間件管道。
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>(); }); } }
如今再測試,咱們發現運行日誌將不會包含異常信息,取而代之的是咱們自定義的信息。
經過本文,咱們知道用戶能夠經過點擊瀏覽器上的中止或從新加載按鈕隨時取消Web應用的請求。而實際上僅僅是終止了客戶端的請求,服務端的請求還在繼續運行。對於簡單耗時短的請求來講,咱們能夠不予理睬。可是,對於耗時任務來講,咱們卻不能夠置若罔聞,由於其有很高的性能損耗。
而如何解決呢?其關鍵是經過CancellationToken
來捕捉用戶請求的狀態,從而根據須要進行相應的處理。
參考資料:
CancellationTokens and Aborted ASP.NET Core Requests
Using CancellationTokens in ASP.NET Core MVC controllers