基于server-sentevents实现服务端消息推送(代码片段)

云来雁去 云来雁去     2023-01-23     246

关键词:

前段时间,为客户定制了一个类似看板的东西,用户可以通过看板了解任务的处理情况,通过APP扫面页面上的二维码就可以领取任务,而当任务被领取以后需要通知当前页面刷新。原本这是一个相对简单的需求,可是因为APP端和PC端是两个不同的Team在维护,换句话说,两个Team各自有一套自己的API接口,前端页面永远无法知道APP到底什么时候扫描了二维码,为此前端页面不得不通过轮询的方式去判断状态是否发生了变化。这种方式会发送大量无用的HTTP请求,因此在最初的版本里,无论是效率还是性能都不能满足业务要求,最终博主采用一种称为服务器推送事件(Server-Sent Events)的技术,所以,在今天这篇文章里,博主相和大家分享下关于服务器推送事件(Server-Sent Events)相关的内容。

什么是Server-Sent Events

我们知道,严格地来讲,HTTP协议是无法做到服务端主动推送消息的,因为HTTP协议是一种请求-响应模型,这意味着在服务器返回响应信息以后,本次请求就已经结束了。可是,我们有一种变通的做法,即首先是服务器端向客户端声明,然后接下来发送的是流信息。换句话说,此时发送的不是一个一次性的数据包,而是以数据流的形式不断地发送过来,在这种情况下,客户端不会关闭连接,会一直等着服务器端发送新的数据过来,一个非常相似而直观的例子是视频播放,它其实就是在利用流信息完成一次长时间的下载。那么,Server-Sent Events(以下简称SSE),就是利用这种机制,使用流信息像客户端推送信息。

说到这里,可能大家会感到疑惑:WebSocket不是同样可以实现服务端向客户端推送信息吗?那么这两种技术有什么不一样呢?首先,WebSocket和SSE都是在建立一种浏览器与服务器间的通信通道,然后由服务器向浏览器推送信息。两者最为不同的地方在于,WebSocket建立的是一个全双工通道,而SSE建立的是一个单工通道。所谓单工和双工,是指数据流动的方向上的不同,对WebSocket而言,客户端和服务端都可以发送信息,所以它是双向通信;而对于SSE而言,只有服务端可以发送消息,故而它是单向通信。从下面的图中我们可以看得更为直观,在WebSocket中数据"有来有往",客户端既可以接受信息亦可发送信息,而在SSE中数据是单向的,客户端只能被动地接收来自服务器的信息。所以,这两者在通信机制上不同到这里已经非常清晰啦!

SSE服务端

下面我们来看看SSE是如何通信的,因为它是一个单工通道的协议,所以协议定义的都是在服务端完成的,我们就从服务端开始吧!协议规定,服务器向客户端发送的消息,必须是UTF-8编码的,并且提供如下的HTTP头部信息:

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

这里出现了一个一种新的MIME类型,text/event-stream。协议规定,第一行的Content-Type必须是text/event-stream,这表示服务端的数据是以信息流的方式返回的,Cache-Control和Connection两个字段和常规的HTTP一致,这里就不再展开说啦!OK,现在客户端知道这是一个SSE信息流啦,那么客户端怎么知道服务端发送了什么消息呢?这就要说到SSE的消息格式,在SSE中消息的基本格式是:

[field]: value\\n

其中,field可以取四个值,它们分别是:dataeventidretry,我们来一起看看它们的用法。

data字段表示数据内容,下面的例子展示SSE中的一行和多行数据,可以注意到,当数据有多行时,可以用\\n作为每一行的结尾,只要保证最后一行以\\n\\n结尾即可。

:这是一行数据内容
data: SSE给你发了一行消息\\n\\n
:这是多行数据内容
data: \\n
data: "foo": "foolish",\\n
data: "bar", 2333\\n
data: \\n\\n

event字段表示自定义事件,默认为message,在浏览器中我们可以用**addEventListener()**来监听响应的事件,这正是为什么SSE被称为服务器推送事件,因为我们在这里既可以发送消息,同样可以发送事件。

: GameStart事件
event: GameStart\\n
data: 敌军还有30秒到达战场\\n\\n

data: Double Kill\\n\\n

: GameOver事件
event: GaneOver\\n
data: You Win!\\n\\n

**id **字段是一个数据标识符,相当于我们可以给每一条消息一个编号。

id: 1\\n
data: 敌军还有30秒到达战场\\n\\n
id: 2\\n
data: Double Kill\\n\\n
id: 3\\n
data: You Win!\\n\\n

retry字段可以指定浏览器重新发起连接的时间间隔,所以,SSE天生就支持断线重连机制。

retry: 10000\\n

SSE客户端

SSE目前是HTML5标准之一,所以,目前主流的浏览器(除了IE和Edge以外)都天然支持这一特性,这意味着我们不需要依赖前端娱乐圈推崇的各种工具链,就可以快速地使用SSE来投入开发。这里需要使用地是EventSource对象,我们从下面这个例子开始了解:

if ('EventSource' in window) 
  var source = new EventSource(url,  withCredentials: true );
  
  /* open事件回调函数 */
  source.onopen = function() 
  	console.log('SSE通道已建立...');
  ;
  
  /* message事件回调函数 */
  source.onmessage = function(evt)
  	console.log(evt.data);
  
  
  /* error事件回调函数 */
  source.onerror = function(evt)
  	console.log('SSE通道发生错误');
  
  
  /* 自定义事件回调 */
  source.addEventListener('foo', function (event) 
  	var data = event.data;
  	// handle message
  ,false);
	
  /* 关闭SSE */
  source.close()

和各种各样的HTML5接口一样,我们需要判断当前的浏览器环境是否支持SSE。建立SSE只需要后端提供一个Url即可,当存在跨域时,我们可以打开第二个参数:withCredentials,这样SSE会在建立通道时携带Cookie。我们通过实例化后的source对象来判断通道是否建立,该对象有一个重要的属性:readyState。当它的取值为0时,表示连接还未建立,或者断线正在重连;当它的取值为1时,表示连接已经建立,可以接受数据;当它的取值为2时,表示连接已断,且不会重连。

好了,当SSE被成功建立以后,首先会触发open事件。这里介绍下SSE中的关键事件,即open、message和error,我们可以分别通过onopenonmessageonerror这三个回调函数来监听相应的事件。对于SSE而言,它是一个单工通道,客户端不能主动向服务端发送信息,所以,一旦建立了SSE通道,客户端唯一需要关注的地方就是onmessage这个回调函数,因为客户端只需要负责处理消息即可,甚至我们可以连onerror都不用关注,因为SSE自带断线重连机制,当然你可以选择在发生错误的时候关掉连接,此时你需要**close()**方法。

我们在上面提到,SSE在服务端可以定义自定义事件,那么,在浏览器中我们该如何接收这些自定义事件呢?这当然要提到无所不能的addEventListener,在人肉操作DOM的jQuery时代,jQuery中提供的大量API在协调不同浏览器间差异的同时,让我们离这些底层的知识越来越远,时至今日,当erySelector/querySelectorAll完全可以替换jQuery的选择器的时候,我们是不是可以考虑重新把某些东西捡起来呢?言归正传,在SSE中,我们只需要像注册普通事件一样,就可以完成对自定义事件的监听,只要客户端和服务端定好消息的协议即可。

在.NET中集成Server-Sent Events

OK,说了这么多,大家一定感觉有一个鲜活的例子会比较好一点,奈何官方提供的示例都是PHP的,难道官方默认PHP是世界上最好的编程语言了吗?所谓万变不离其宗",下面我们以.NET为例来快速集成Server-Sent Events,这里需要说明的是,博主下面的例子采用ASP.NET Core 2.0版本编写,首先,我们建一个名为SSEController的控制器,在默认的Index()方法中,按照SSE规范,我们首先组织HTTP响应头,然后发送了一个名为SSE_Start的自定义事件,接下来,我们每隔10秒钟给客户端发送一条消息,请原谅我如此敷衍的Sleep():

[Route("api/[controller]")]
[ApiController]
public class SSEController : Controller

    [HttpGet]
    public IActionResult Index()
    
    	//组织HTTP响应头
    	Response.Headers.Add("Connection", "keep-alive");
    	Response.Headers.Add("Cache-Control", "no-cache");
    	Response.Headers.Add("Content-Type", "text/event-stream");

    	//发送自定义事件
        var message = BuildSSE(new  Content = "SSE开始发送消息", Time = DateTime.Now , "SSE_Start");
        Response.Body.Write(message, 0, message.Length);

        //每隔10秒钟向客户端发送一条消息
        while (true)
        
            message = BuildSSE(new  Content = $"当前时间为DateTime.Now" );
            Response.Body.Write(message, 0, message.Length);
            Thread.Sleep(10000);
       
    

我们提到,SSE的数据是按照一定的格式,由id、event、data和retry四个字段构成的,那么,织消息格式的代码我们放在了**BuildSSE()**方法中,我们来一起看看它的实现:

private byte[] BuildSSE<TMessage>(TMessage message, string eventName = null, int retry = 30000)

    var builder = new StringBuilder();
    builder.Append($"id:Guid.NewGuid().ToString("N")\\n");
    if (!string.IsNullOrEmpty(eventName))
        builder.Append($"event:eventName\\n");
    builder.Append($"retry:retry\\n");
    builder.Append($"data:JsonConvert.SerializeObject(message)\\n\\n");
    return Encoding.UTF8.GetBytes(builder.ToString());
 

可以看到,完全按照SSE规范来定义的,这里每次生成一个新的GUID来作为消息的ID,客户端断线后重连的间隔为30秒,默认发送的是**“消息”**,当指定eventName参数时,它就表示一个自定义事件,这里我们使用JSON格式来传递信息。好了,这样我们就完成了服务端的开发,怎么样,是不是感觉非常简单呢?我们先让它跑起来,下面着手来编写客户端,这个就非常简单啦!

<!DOCTYPE html>
<html>
<body>
<h1>DotNet-SSE</h1>
<div id="result"></div>
<script>
if ('EventSource' in window) 
  var source = new EventSource('http://localhost:5000/api/SSE/');
  
  /* open事件回调函数 */
  source.onopen = function() 
  	document.getElementById("result").innerHTML+= "SSE通道已建立...<br/>";
  ;
  
  /* message事件回调函数 */
  source.onmessage = function(evt)
  	document.getElementById("result").innerHTML+= "Message: " + event.data + "<br/>";
  
  
  /* error事件回调函数 */
  source.onerror = function(evt)
      document.getElementById("result").innerHTML+= "SSE通道发生错误<br/>";
  
  
  /* SSE_Start事件回调 */
  source.addEventListener('SSE_Start', function (event) 
  	document.getElementById("result").innerHTML += "SSE_Start: " + event.data + "<br/>";
  ,false);

</script>
</body>
</html>

此时,不需要任何现代前端方面的技术,我们直接打开浏览器,就可以看到:

更为直观的,我们可以通过Chrome开发者工具观察到实际的请求情况,相比普通的HTTP请求,SSE会出现一个名为EventStream的选项卡,这是因为我们在服务端设置的Content-Type为text/event-stream的缘故,可以注意到,我们定义的id(GUID)会在这里显示出来:

同类技术优劣对比

OK,这篇文章写到这里,相信大家已经对SSE有了一个比较具体的概念,那么,我们不妨来梳理下相关的同类技术。一路走过来,我们大体上经历了**(短)轮询**、长轮询/CometSSEWebSocket

(短)轮询这个比较容易理解了,它从本质上来讲,就是由客户端定时去发起一个HTTP请求,这种方式是一种相对尴尬的方式,为什么这样说呢?因为时间间隔过长则无法保证数据的时效性,而时间间隔过短则会发送大量无用的请求,尤其是当客户端数量比较多的时候,这种方式很容易耗尽服务器的连接数。

而长轮询则是(短)轮询的一个变种,它和(短)轮询最大的不同在于,服务端在接收到请求以后,并非立即进行响应,而是先将这个请求挂起,直到服务器端数据发生变化时再进行响应。所以,一个明显的优势是,它相对地减少了大量不必要的HTTP请求,那么,它是不是就完美无暇了呢?当然不是,因为服务端会将客户端发来的请求挂起,因此在挂起的那些时间里,服务器的资源实际上是被浪费啦!

严格地说,SSE并不是一门新技术,为什么这样说呢?因为它和我们基于HTTP长连接的Push非常相似。这里又提到一个新概念,HTTP长连接,其实,这个说法病逝非常严谨,因为我们知道HTTP最早就是一个请求-响应模型,直到HTTP1.1中增加了持久连接,即Connection:keep-alive的支持。所以,我们这里说的长连接、短链接实际上都是指TCP的长连接还是短连接,换句话说,它和客户端没有关系,只要服务端支持长连接,那么在某个时间段内的TCP连接实际上复用的,进而就能提高HTTP请求性能,曾经我们不是还用iframe做过长连接吗?

WebSocket作为构建实时交互应用的首选技术,博主曾经在《基于WebSocket和Redis实现Bilibili弹幕效果》一文中有所提及,WebSocket相比前面这些技术,最大的不同在于它拥有专属的通信通道,一旦这个通道建立,客户端和服务端就可以互相发送消息,它沿用了我们传统的Socket通讯的概念和原理,变被动为主动,无论是客户端还是服务端,都不必再被动地去**“拉"或者"推”**。在这个过程中,出现了像SignalR/SocketIO等等的库,它们主打的兼容性和降级策略,曾经一度让我们感到亲切,不过随着WebSocket标准化的推进,相信这些最终都会被原生API所替代吧,也许是有生之年呢?谁知道未来是什么样子呢?

下面给出针对以上内容的**“简洁”**版本:

(短)轮询长轮询/CometSSEWebSocket
浏览器支持全部全部除IE/Edge现代浏览器
是否独立协议HTTPHTTPHTTPWS
是否轻量
断线重连
负载压力占用内存/请求数同(短)轮询一般同SSE
数据延迟取决于请求间隔同(短)轮询实时实时

本文小结

正如本文一开始所写,博主使用SSE是因为业务上的需要,在经历了轮询带来的性能问题以后,博主需要一款类似WebSocket的东西,来实现服务端主动向客户端推送消息,究其原因,是因为浏览器永远都不知道,App到底什么时候会扫描二维码,所以,从一开始我们试图让网页去轮询的做法,本身就是不太合理的。那么,为什么没有用WebSocket呢?因为WebSocket需要一点点框架层面的支持,所以,我选择了更为轻量级的SSE,毕竟,这比让其它Team的同事去调整他们的后端接口要简单的多。我之前参与过一部分WebSocket相关的项目,我深切地感受到,除了在浏览器的兼容性问题以外,因为WebSocket使用的是独有的WS协议,所以,我们常规的API网关其实在这方面支持的都不是很好,更不用说鉴权、加密等等一系列的问题啦,而SSE本身是基于HTTP协议的,我们目前针对HTTP的各种基础设施,都可以直接拿过来用,这应该是我最大的一点感悟了吧,好了,这篇文章就是这样啦,谢谢大家,新的一年注定要重新开始的呢…

参考文章

逐句回答,流式返回,chatgpt采用的server-sentevents后端实时推送协议python3.10实现,基于tornado6.1(代码片段)

...到前端用户反馈,同时也可以缓解连接超时的问题。Server-sentevents(SSE)是一种用于实现服务器到客户端的单向通信的协议。使用SSE,服务器可以向客户端推送实时数据,而无需客户端发出请求。SSE建立在HTTP... 查看详情

server-sentevent后台用.net怎样实现

参考技术AHTML5有一个Server-SentEvents(SSE)功能,允许服务端推送数据到客户端。(通常叫数据推送)。我们来看下,传统的WEB应用程序通信时的简单时序图:sse1现在WebApp中,大都有Ajax,是这样子:sse2基于数据推送是这样的,当数据源... 查看详情

基于sse实现服务端消息主动推送解决方案(代码片段)

一、SSE服务端消息推送SSE是Server-SentEvents的简称,是一种服务器端到客户端(浏览器)的单项消息推送。对应的浏览器端实现EventSource接口被制定为HTML5的一部分。不过现在IE不支持该技术,只能通过轮训的方式实现。相比于W... 查看详情

html5服务器发送事件(server-sentevents)

HTML5服务器发送事件(Server-SentEvents)HTML5服务器发送事件(server-sentevent)允许网页获得来自服务器的更新。Server-Sent事件-单向消息传递Server-Sent事件指的是网页自动获取来自服务器的更新。以前也可能做到这一点,前提是网页不得... 查看详情

html5服务器发送事件(server-sentevents)

HTML5服务器发送事件(Server-SentEvents)HTML5服务器发送事件(server-sentevent)允许网页获得来自服务器的更新。Server-Sent事件-单向消息传递Server-Sent事件指的是网页自动获取来自服务器的更新。以前也可能做到这一点,前提是网页不得... 查看详情

html5:html5服务器发送事件(server-sentevents)

ylbtech-HTML5: HTML5服务器发送事件(Server-SentEvents) 1.返回顶部1、HTML5 服务器发送事件(Server-SentEvents)HTML5服务器发送事件(server-sentevent)允许网页获得来自服务器的更新。Server-Sent事件-单向消息传递Server-Sent事件指的是网... 查看详情

server-sentevents(html5服务器发送事件)

Server-SentEvents简介Server-SentEvents(SSE)用于网页自动获取服务器上更新的数据,它是一个实时性的机制。实时性获取数据的解决方案对于某些需要实时更新的数据(例如Facebook/Twitter更新、估价更新、新的博文、赛事结果等)来说,... 查看详情

html5服务器发送事件(server-sentevents)

沈阳SEO:HTML5服务器发送事件(server-sentevent)允许网页获得来自服务器的更新。Server-Sent事件-单向消息传递Server-Sent事件指的是网页自动获取来自服务器的更新。以前也可能做到这一点,前提是网页不得不询问是否有可用的更新... 查看详情

html5服务器推送事件(server-sentevents)实战开发

转自:http://www.ibm.com/developerworks/cn/web/1307_chengfu_serversentevent/http://www.ibm.com/developerworks/cn/web/wa-lo-comet/  --comet长连接服务器推送事件(Server-sentEvents)是HTML5规范中的一个组成部分,可以用来从服务端实 查看详情

web端即时通讯实践:实现单机几十万条长连接

...orModel来管理长连接、由服务器主动发送事件的。SSE(Server-sentevents)技术简介服务器发送事件(Server-sentevents,SSE)是一种客户端服务器之间的通信技术(详见即时通讯网整理的文章《SSE技术详解:一种全新... 查看详情

webworker和server-sentevents

...入输出,像磁盘存取数据)webworker的详细用法——阮一峰Server-SentEvents简称sse,sse的主要作用是方便服务端向客户端推送信息。轮询、长轮询、sse、WebSocket区别。轮询:就是不停的请求,有新数据就更新,没有就为空长轮询:就... 查看详情

php+sse服务器向客户端推送消息

...现原理客户端代码服务端代码源码index.htmlsse.php阐述SSE(server-sentevent)是基于HTML5的服务器推送消息事件,它允许服务端单向向浏览器客户端发送数据,SSE使用流信息向浏览器推送信息,浏览器自动接收服务端推送过来的消息,... 查看详情

基于sse实现服务端消息主动推送解决方案(代码片段)

一、SSE服务端消息推送SSE是Server-SentEvents的简称,是一种服务器端到客户端(浏览器)的单项消息推送。对应的浏览器端实现EventSource接口被制定为HTML5的一部分。不过现在IE不支持该技术,只能通过轮训的方式实现。相比于W... 查看详情

html5sse

 HTML5服务器发送事件(Server-SentEvents)服务器发送事件(Server-sentEvents)是基于WebSocket协议的一种服务器向客户端发送事件和数据的单向通讯。HTML5服务器发送事件(server-sentevent)允许网页获得来自服务器的更新。Ser... 查看详情

php+sse服务器向客户端推送消息

...现原理客户端代码服务端代码源码index.htmlsse.php阐述SSE(server-sentevent)是基于HTML5的服务器推送消息事件,它允许服务端单向向浏览器客户端发送数据,SSE使用流信息向浏览器推送信息,浏览器自动接收服务端推送过来的消息,... 查看详情

基于c#实现windows服务

...虑做成一个Windows服务。这篇文章跟大家介绍一下,如何基于C#实现Windows服务的创建、安装、启动、停止和卸载。Windows服务介绍MicrosoftWindows服务能够创 查看详情

即时通讯开发技术sse详解:html5服务器推送事件技术

...:传统Ajax短轮询、Comet技术、WebSocket技术、SSE(Server-sentEvents)。服务器推送事件(Server-sentEvents)ÿ 查看详情

基于kcp,consul的servicemesh实现

...端代理服务层,包括外部的service后端实现负债均衡kcpkcp基于udp,能够实现快速的传输consul实现了服务注册,服务的健康检查,多中心外部服务的注册外部服务要注册到前端代理中,前端代理抛给其他服务调用外部服务可以通过... 查看详情