最近用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);} }
?