最近用asp.net core webapi實現了一個實時視頻流的推送功能,在Asp.net中,這個是通過PushStreamContent來實現的。
基於對asp.net core的知識,隨手寫了一個(要求控制器繼承自Controller基類)
[HttpGet] public async Task Get() { var response = HttpContext.Response; response.ContentType = "text/html"; response.StatusCode = 200; var stream = HttpContext.Response.Body; while (true) { await Task.Delay(1000); var content = DateTime.Now + @"<br>"; var data = Encoding.Default.GetBytes(content); await stream.WriteAsync(data, 0, data.Length); await stream.FlushAsync(); } }
使用chrome調試這個接口時,發現它確實行之有效的將當前的時間推送到了瀏覽器的頁面上。
然而,當我進一步的調試它的異常情況時,發現就算將chrome關掉,這個程序卻依然在繼續運行。從調試器中看到stream的狀態為Aborted,已經識別到位終止的流了。
並且從VS的調試窗口也能看到異常信息:
但下面這兩行就是不拋異常:
await stream.WriteAsync(data, 0, data.Length); await stream.FlushAsync();
單單從接口的實現角度上來看,這個已經不合理了。這是一個很大的坑,功能看上去還是正確的,沒有詳細調試還看不出來。一個不留神就踩上了。不知道微軟為什么要這么設計。
埋怨歸埋怨,問題還是要解決的。我查看了下FileStreamResult的源碼,發現它是靠HttpContext.RequestAborted來判斷客戶端是否終止了的。這是一個CancellationToken類型的對象,當客戶端連接斷開后,它就處於被取消的狀態。
知道原因后,就可以知道如何修改我的程序了。
[HttpGet] public async Task Get() { var cancel = HttpContext.RequestAborted; var response = HttpContext.Response; response.ContentType = "text/html"; response.StatusCode = 200; var stream = HttpContext.Response.Body; while (true) { cancel.ThrowIfCancellationRequested(); await Task.Delay(1000, cancel); var content = DateTime.Now + @"<br>"; var data = Encoding.Default.GetBytes(content); await stream.WriteAsync(data, 0, data.Length, cancel); await stream.FlushAsync(cancel); } }
再然后就是封裝了,我這里將其封裝為了一個PushStreamResult,這樣就可以在PocoController中使用了。
class MyPushStreamResult :IActionResult { Func<Stream, CancellationToken, Task> _pushAction; string _contentType; public MyPushStreamResult(Func<Stream, CancellationToken, Task> pushAction, string contentType) { _pushAction = pushAction; _contentType = contentType; } public Task ExecuteResultAsync(ActionContext context) { var response = context.HttpContext.Response; response.ContentType = _contentType; response.StatusCode = 200; return _pushAction(response.Body, context.HttpContext.RequestAborted); } }
使用方法如下:
[HttpGet] public IActionResult Get() { return new MyPushStreamResult(pushData, "text/html"); } async Task pushData(Stream stream, CancellationToken cancel) { while (true) { if (cancel.IsCancellationRequested) return; await Task.Delay(1000, cancel); var content = DateTime.Now + @"<br>"; var data = Encoding.Default.GetBytes(content); await stream.WriteAsync(data, 0, data.Length, cancel); await stream.FlushAsync(cancel); } }