https://www.w3schools.com/html/html5_serversentevents.asp
https://stackoverflow.com/questions/36227565/aspnet-core-server-sent-events-response-flush
https://www.smashingmagazine.com/2018/02/sse-websockets-data-flow-http2/
https://stackoverflow.com/a/58565850
//<body> // <script type="text/javascript"> var source = new EventSource("/sse"); source.addEventListener("message", (event) => console.log(event.data)); source.addEventListener("open", (event) => console.log("SSE open", event)); source.addEventListener("close", (event) => console.log("SSE close", event)); source.addEventListener("error", function (e) { if (e.readyState == EventSource.CLOSED) { // Connection was closed. console.log("SSE: connection closed"); } else { console.log(e); } }, false); // </script> //</body>
public class Counter { public Counter(int start = 0, int resetAt = 100000) { counter = start; reset = resetAt; } private volatile int counter; private int reset; public int Current => counter; public int GetNext() { counter += 1; counter %= reset; return counter; } } [HttpGet("html")] public ContentResult GetHtml() { var html = string.Join(Environment.NewLine, new string[] { "<body>", "<script type=\"text/javascript\">", "var source = new EventSource(\"/WeatherForecast/stream\");", "source.addEventListener(\"message_a\", (event) => console.log('message_a', event.data, event.lastEventId));", "source.addEventListener(\"message\", (event) => console.log(event.data));", "source.addEventListener(\"open\", (event) => console.log(\"SSE open\", event));", "source.addEventListener(\"close\", (event) => console.log(\"SSE close\", event));", "source.addEventListener(\"error\", (event) => console.log(\"SSE error\", event));", "</script>", "</body>" }); return Content(html, "text/html"); } public static ConcurrentDictionary<int, HttpResponse> Responses = new ConcurrentDictionary<int, HttpResponse>(); public static Counter ResponseCounter = new Counter(); public static Counter EventCounter = new Counter(); public static void SendEvent() { if (!Responses.Any()) return; var rng = new Random(); int delay = 100 * rng.Next(3, 40); var json = JsonConvert.SerializeObject(new { random = rng.NextDouble() * 10, delay = delay }); var i = EventCounter.GetNext(); var message = string.Join(Environment.NewLine, new string[] { $"id: {i}", "event: message_a", $"data: {json}", "\n" }); foreach(var kv in Responses) { Task.Run(async () => { try { await kv.Value.WriteAsync(message); } catch(ObjectDisposedException ex) { Responses.Remove(kv.Key, out var _); Console.WriteLine(ex.ToString()); // Should not occur by default } }); } } [HttpGet("stream")] public async Task GetStream() { var response = Response; var key = ResponseCounter.GetNext(); Responses.TryAdd(key, response); response.Headers.Add("Content-Type", "text/event-stream"); var cancellationToken = HttpContext.RequestAborted; await Task.Run(() => { cancellationToken.WaitHandle.WaitOne(); Responses.Remove(key, out var _); }); }
Middleware
// Server with middleware app.Use(async (context, next) => { if (context.Request.Path.ToString().Equals("/sse")) { var response = context.Response; response.Headers.Add("Content-Type", "text/event-stream"); // Connection: keep-alive for(var i = 0; true; ++i) { // WriteAsync requires `using Microsoft.AspNetCore.Http` await response .WriteAsync($"data: Middleware {i} at {DateTime.Now}\r\r"); await response.Body.FlushAsync(); await Task.Delay(5 * 1000); } } await next.Invoke(); });
488300cookie-checkServer Sent Events (SSE)