基于http协议交互的推送方法大概方法如下:
- 轮询(ajax),比较耗费服务器资源。COMET方式(COMET 技术并不是 HTML 5 )
- websocket 双向数据推送,灵活,功能强大
- Server-sent-event(简称SSE),单项数据推送(Server-sent Events 规范是 HTML 5 规范的一个组成部分)
这里我们研究一下SSE;
一、什么是SSE
Server-sent Events 规范是 HTML 5 规范的一个组成部分,具体的规范文档见参考资源。该规范比较简单,主要由两个部分组成:第一个部分是服务器端与浏览器端之间的通讯协议,第二部分则是在浏览器端可供 JavaScript 使用的 EventSource 对象。通讯协议是基于纯文本的简单协议。服务器端的响应的内容类型是“text/event-stream”。响应文本的内容可以看成是一个事件流,由不同的事件所组成。每个事件由类型和数据两部分组成,同时每个事件可以有一个可选的标识符。不同事件的内容之间通过仅包含回车符和换行符的空行(“\r\n”)来分隔。每个事件的数据可能由多行组成。严格地说,HTTP协议无法做到服务器主动推送信息。但是有一种变通的发光法,就是服务器向客户端声明,接下来要发送的是流信息,也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断的发送过来。这是客户端不会关闭连接,会一直等待服务器发过来的数据流,视频播放就是这样的例子。本质上这种通信就是以流信息的方式,完成一次用时很长的下载。
二、SSE传输协议分析
了解了什么是SSE之后就发现这种模式针对后端开发来说是一个巨大的改进,可以像ajax一样,却比ajax节省资源;能实现websocket的服务器推送却不需要更换协议和端口,就像写一个特别点的api接口一样方便。跟踪一下sse的报文显示,
1 : this is a comment\n 2 reply: 3000\n 3 event: message\n 4 data: first\n\n 5 data: second\n\n 6 id: 100\n 7 event: myevent\n 8 data: third\n\n 9 id: 101\n 10 : this is a comment\n 11 data: fourth\n 12 data: fourth continue\n\n
接下就按如下来分析报文内容:
类型为空白,表示该行是注释,会在处理时被忽略。
类型为 data,表示该行包含的是数据。以 data 开头的行可以出现多次。所有这些行都是该事件的数据。
类型为 event,表示该行用来声明事件的类型。浏览器在收到数据时,会产生对应类型的事件。
类型为 id,表示该行用来声明事件的标识符。
类型为 retry,表示该行用来声明浏览器在连接断开之后进行再次连接之前的等待时间。
三、C#实现SSE服务端
SSE的内容还是很简洁的,了解了差不多了,现在开始做起来。
1.根据SSE规范对html的头部进行处理,主要就是添加text/event-stream类型,去掉缓存
1 HttpContext.Current.Response.ContentType = "text/event-stream; charset=utf-8"; 2 HttpContext.Current.Response.SetHeader(ResponseHeaderType.CacheControl, "no-cache"); 3 HttpContext.Current.Response.SetHeader(ResponseHeaderType.KeepAlive, "timeout=5"); 4 HttpContext.Current.Response.Status = HttpStatusCode.OK; 5 HttpContext.Current.Response.SendHeader(-1);
2.封装SSE数据格式,SSE的数据都是采用UTF8进行处理的
1 ServerSent(Encoding.UTF8.GetBytes($"id: {id?.Trim()}\nevent: {@event?.Trim()}\ndata: {SerializeHelper.Serialize(t)}\n\n"));
仅需二步就已经完成了SSE服务端的处理了,下面是SAEA.MVC下面的一个完整封装类EventStream
1 /**************************************************************************** 2 *项目名称:SAEA.MVC 3 *CLR 版本:4.0.30319.42000 4 *机器名称:WALLE-PC 5 *命名空间:SAEA.MVC 6 *类 名 称:EventStream 7 *版 本 号:V1.0.0.0 8 *创建人: yswenli 9 *电子邮箱:[email protected] 10 *创建时间:2021/1/6 14:02:09 11 *描述: 12 *===================================================================== 13 *修改时间:2021/1/6 14:02:09 14 *修 改 人: yswenli 15 *版 本 号: V1.0.0.0 16 *描 述: 17 *****************************************************************************/ 18 using SAEA.Common; 19 using SAEA.Common.Serialization; 20 using SAEA.Common.Threading; 21 using SAEA.Http.Model; 22 using System.Net; 23 using System.Text; 24 25 namespace SAEA.MVC 26 { 27 /// <summary> 28 /// SSE服务器事件流 29 /// </summary> 30 public class EventStream : ActionResult, IEventStream 31 { 32 /// <summary> 33 /// 最后一次接收到的事件的标识符 34 /// </summary> 35 public int LastEventID 36 { 37 get; 38 private set; 39 } 40 41 /// <summary> 42 /// SSE服务器事件流 43 /// </summary> 44 /// <param name="retry">指定浏览器重新发起连接的时间间隔</param> 45 public EventStream(int retry = 3 * 1000) 46 { 47 this.ContentEncoding = Encoding.UTF8; 48 49 if (HttpContext.Current.Request.Headers.ContainsKey("Last-Event-ID")) 50 { 51 if (int.TryParse(HttpContext.Current.Request.Headers["Last-Event-ID"], out int id)) 52 { 53 LastEventID = id; 54 } 55 } 56 57 HttpContext.Current.Response.ContentType = "text/event-stream; charset=utf-8"; 58 HttpContext.Current.Response.SetHeader(ResponseHeaderType.CacheControl, "no-cache"); 59 HttpContext.Current.Response.SetHeader(ResponseHeaderType.KeepAlive, "timeout=5"); 60 HttpContext.Current.Response.Status = HttpStatusCode.OK; 61 HttpContext.Current.Response.SendHeader(-1); 62 63 //心跳 64 var pong = $"SAEAServer PONG {DateTimeHelper.Now:yyyy:MM:dd HH:mm:ss.fff}"; 65 66 TaskHelper.LongRunning(() => 67 { 68 ServerSent(Encoding.UTF8.GetBytes($": {SerializeHelper.Serialize(pong)}\n\n")); 69 }, 1000); 70 71 //断开重连时长 72 ServerSent(Encoding.UTF8.GetBytes($"retry: {retry}\n\n")); 73 } 74 /// <summary> 75 /// 发送通知 76 /// </summary> 77 /// <param name="str"></param> 78 /// <param name="event"></param> 79 /// <param name="id"></param> 80 public void ServerSent<T>(T t, string @event = "message", string id = "") where T : class 81 { 82 if (t != null) 83 ServerSent(Encoding.UTF8.GetBytes($"id: {id?.Trim()}\nevent: {@event?.Trim()}\ndata: {SerializeHelper.Serialize(t)}\n\n")); 84 } 85 /// <summary> 86 /// 发送通知 87 /// </summary> 88 /// <param name="content"></param> 89 public void ServerSent(byte[] content) 90 { 91 HttpContext.Current.Response.SendData(content); 92 } 93 } 94 }
3.使用EventStream类快速实现服务器推送
将EventStream集成到Controller中,那么在业务继承类中就可以直接使用封装好的SSE功能了,如下例:
1 /**************************************************************************** 2 *项目名称:SAEA.MVCTest.Controllers 3 *CLR 版本:4.0.30319.42000 4 *机器名称:WALLE-PC 5 *命名空间:SAEA.MVCTest.Controllers 6 *类 名 称:EventStreamController 7 *版 本 号:V1.0.0.0 8 *创建人: yswenli 9 *电子邮箱:[email protected] 10 *创建时间:2021/1/6 13:57:09 11 *描述: 12 *===================================================================== 13 *修改时间:2021/1/6 13:57:09 14 *修 改 人: yswenli 15 *版 本 号: V1.0.0.0 16 *描 述: 17 *****************************************************************************/ 18 using SAEA.MVC; 19 using System; 20 using System.Collections.Generic; 21 using System.Text; 22 using System.Threading; 23 24 namespace SAEA.MVCTest.Controllers 25 { 26 /// <summary> 27 /// EventStreamController 28 /// </summary> 29 public class EventStreamController : Controller 30 { 31 /// <summary> 32 /// 发送通知 33 /// </summary> 34 /// <returns></returns> 35 public ActionResult SendNotice() 36 { 37 try 38 { 39 var es = GetEventStream(); 40 41 for (int i = 0; ; i++) 42 { 43 var str = $"SAEA.MVC EventStream Test {i}"; 44 45 es.ServerSent(str); 46 47 Thread.Sleep(1000); 48 } 49 } 50 catch (Exception ex) 51 { 52 53 } 54 return Empty(); 55 } 56 } 57 }
四、验证SSE功能
了解了SSE技术相关理论,并按理论封装了EventStream,最后使用EventStream实现了一个推送测试逻辑,接下来就是使用js的EventSource对象在浏览器中来验证了。
创建一个网页,在html中的js中输入:
1 var source = new EventSource("/api/eventstream/sendnotice"); 2 source.onmessage = function (event) { 3 document.getElementById("eventstream").innerHTML += event.data + "<br/>"; 4 };
打开浏览器的工发者工具,在网络选项中查看详细内容: