因为 HTTP/1.1 自己不支持服务器主动向客户端推送消息,在例如即时通信、消息提醒等应用场景中就会很不方便。解决的方法有不少,WebSocket 就很不错,可是若是想要快速实现的话就不推荐使用,本文要介绍的是一种轻量的解决方案:SSE。javascript
SSE 是基于 HTTP 协议来完成服务器推送的。不要误会,并非说 HTTP/2,而是一种取巧的方式:当服务器向客户端声明接下来要发送流信息时,客户端就会保持链接打开,SSE 使用的就是这种原理。java
理论上,SSE 和 WebSocket 作的是同一件事情。当你须要用新数据局部更新网络应用时,SSE 能够作到不须要用户执行任何操做,即可以完成。浏览器
举例咱们要作一个统计系统的管理后台,咱们想知道统计数据的实时状况。相似这种更新频繁、低延迟的场景,SSE 能够彻底知足。服务器
其余一些应用场景:例如邮箱服务的新邮件提醒,微博的新消息推送、管理后台的一些操做实时同步等,SSE 都是不错的选择。网络
SSE 是单向通道,只能服务器向客户端发送消息,若是客户端须要向服务器发送消息,则须要一个新的 HTTP 请求。这对比 WebSocket 的双工通道来讲,会有更大的开销。这么一来的话就会存在一个「何时才须要关心这个差别?」的问题,若是平均每秒会向服务器发送一次消息的话,那应该选择 WebSocket。若是一分钟仅 5 - 6 次的话,其实这个差别并不大。socket
在浏览器兼容方面,二者差很少。在较早以前,每当须要创建双向 Socket 时就会使用 Flash,在移动浏览器不支持 Flash 的状况下,WebSocket 的兼容是比较难作的。ide
SSE 我认为最大的优点是便利:ui
有了这些优点,在选择使用 SSE 时就已经为本身的项目节约了很多成本。编码
下面是一个简单的示例,实现一个 SSE 服务。url
'use strict'; const http = require('http'); http.createServer((req, res) => { // 服务器声明接下来发送的是事件流 res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*', }); // 发送消息 setInterval(() => { res.write('event: slide\n'); // 事件类型 res.write(`id: ${+new Date()}\n`); // 消息ID res.write('data: 7\n'); // 消息数据 res.write('retry: 10000\n'); // 重连时间 res.write('\n\n'); // 消息结束 }, 3000); // 发送注释保持长链接 setInterval(() => { res.write(': \n\n'); }, 12000); }).listen(2000);
服务器首先向客户端声明接下来发送的是事件流(text/event-stream)类型的数据,而后就能够向客户端屡次发送消息。
事件流是一个简单的文本流,仅支持 UTF-8 格式的编码。每条消息以一个空行做为分隔符。
在规范中为消息定义了 4 个字段:
event 消息的事件类型。客户端收到消息时,会在当前的 EventSource 对象上触发一个事件,这个事件的名称就是这个字段的值,若是消息没有这个字段,客户端的 EventSource 对象就会触发默认的 message 事件。
id 这条消息的 ID。客户端接收到消息后,会把这个 ID 做为内部属性 Last-Event-ID
,在断开重连成功后,会把 Last-Event-ID
发送给服务器。
data 消息的数据字段。客户端会把这个字段解析为字符串,若是一条消息有多个 data 字段,客户端会自动用换行符链接成一个字符串。
retry 指定客户端重连的时间。只接受整数,单位是毫秒。若是这个值不是整数则会被自动忽略。
一个颇有意思的地方是,规范中规定以冒号开头的消息都会被看成注释,一条普通的注释(:\n\n
)对于服务器来讲只占 5 个字符,可是发送到客户端上的时候不会触发任何事件,这对客户端来讲是很是友好的。因此注释通常被用于维持服务器和客户端的长链接。
咱们建立了一个 EventSource
对象,传入参数:url
。而且根据服务器的状态和发送的信息做出响应。
'use strict'; if (window.EventSource) { // 建立 EventSource 对象链接服务器 const source = new EventSource('http://localhost:2000'); // 链接成功后会触发open事件 source.addEventListener('open', () => { console.log('Connected'); }, false); // 服务器发送信息到客户端时,若是没有event字段,默认会触发message事件 source.addEventListener('message', e => { console.log(`data: ${e.data}`); }, false); // 自定义EventHandler,在收到event字段为slide的消息时触发 source.addEventListener('slide', e => { console.log(`data: ${e.data}`); // => data: 7 }, false); // 链接异常时会触发error事件并自动重连 source.addEventListener('error', e => { if (e.target.readyState === EventSource.CLOSED) { console.log('Disconnected'); } else if (e.target.readyState === EventSource.CONNECTING) { console.log('Connecting...'); } }, false); } else { console.error('Your browser doesn\'t support SSE'); }
EventSource从父接口 EventTarget 中继承了属性和方法,其内置了 3 个 EventHandler 属性、2 个只读属性和 1 个方法:
EventSource.onopen 在链接打开时被调用。
EventSource.onmessage 在收到一个没有 event 属性的消息时被调用。
EventSource.onerror 在链接异常时被调用。
EventSource.readyState 一个 unsigned short 值,表明链接状态。可能值是CONNECTING (0), OPEN (1), 或者 CLOSED (2)。
EventSource.url 链接的 URL。
EventSource.close() 关闭链接。
客户端在每次接收到消息时,会把消息的 id 字段做为内部属性 Last-Event-ID
储存起来。
SSE 默认支持断线重连机制,在链接断开时会触发EventSource 的 error 事件,同时自动重连。再次链接成功时 EventSource 会把 Last-Event-ID
属性做为请求头发送给服务器,这样服务器就能够根据这个 Last-Event-ID
做出相应的处理。
这里须要注意的是,id 字段不是必须的,服务器有可能不会在消息中带上 id 字段,这样子客户端就不会存在 Last-Event-ID
这个属性。因此为了保证数据可靠,咱们须要在每条消息上带上 id 字段。
在 SSE 的草案中提到,"text/event-stream" 的 MIME 类型传输应当在静置 15 秒后自动断开。在实际的项目中也会有这个机制,可是断开的时间没有被列入标准中。
为了减小服务器的开销,咱们也能够有目的的断开和重连。
简单的办法是服务器发送一个关闭消息并指定一个重连的时间戳,客户端在触发关闭事件时关闭当前链接并建立一个计时器,在重连时把计时器销毁。
'use strict'; function connectSSE() { if (window.EventSource) { const source = new EventSource('http://localhost:2000'); let reconnectTimeout; source.addEventListener('open', () => { console.log('Connected'); clearTimeout(reconnectTimeout); }, false); source.addEventListener('pause', e => { source.close(); const reconnectTime = +e.data; const currentTime = +new Date(); reconnectTimeout = setTimeout(() => { connectSSE(); }, reconnectTime - currentTime); }, false); } else { console.error('Your browser doesn\'t support SSE'); } } connectSSE();
Broswer support of EventSource from Can I Use...
早些时候,为了实现数据实时更新最多见的方法就是轮询。
轮询是以一个固定频率向服务器发送请求,服务器在有数据更新时返回新的数据,以此来管理数据的更新。这种轮询的方式不但开销大,并且更新的效率和频率有关,也不能达到及时更新的目的。
接着便出现了长轮询的方式:客户端向服务器发送请求以后,服务器会暂时把请求挂起,等到有数据更新时再返回最新的数据给客户端,客户端在接收到新的消息后再向服务器发送请求。与常规轮询的不一样之处是:数据能够作到实时更新,能够减小没必要要的开销。
这里有一个「选择长轮询仍是常规轮询?」的命题,长轮询是否是总比常规轮询占有优点?咱们能够从带宽占用的角度分析,若是一个程序数据更新太过频繁,假设每秒 2 次更新,若是使用长轮询的话每分钟要发送 120 次 HTTP 请求。若是使用常规轮询,每 5 秒发送一次请求的话,一分钟才 20 次,从这里看,常规轮询更占有优点。
长轮询和 SSE 最关键的区别在于,每一次数据更新都须要一次 HTTP 请求。和 WebSocket 还有 SSE 同样,长轮询也会占用一个 socket。在数据更新效率上和 SSE 差很少,一有数据更新就能检测到。加上全部浏览器都支持,是一个不错的 SSE 替代方案。
文章介绍了 SSE 的用法及使用过程当中的一些技巧。对比 WebSocket,SSE 在开发时间和成本上占有较大的优点。作数据推送服务,除了 WebSocket,SSE 也是一个不错的选择,但愿对你们有所帮助。
Server-Sent Events
EventSource - Web APIs | MDN
Using server-sent events - Web APIs | MDN
版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证) 转载请注明出处
原文地址:20 行代码写一个数据推送服务
文章做者:何启邦
版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证) 转载请注明出处