20 行代码写一个数据推送服务

因为 HTTP/1.1 自己不支持服务器主动向客户端推送消息,在例如即时通信、消息提醒等应用场景中就会很不方便。解决的方法有不少,WebSocket 就很不错,可是若是想要快速实现的话就不推荐使用,本文要介绍的是一种轻量的解决方案:SSE。javascript

SSE 是基于 HTTP 协议来完成服务器推送的。不要误会,并非说 HTTP/2,而是一种取巧的方式:当服务器向客户端声明接下来要发送流信息时,客户端就会保持链接打开,SSE 使用的就是这种原理。java

SSE 能作什么

理论上,SSE 和 WebSocket 作的是同一件事情。当你须要用新数据局部更新网络应用时,SSE 能够作到不须要用户执行任何操做,即可以完成。浏览器

举例咱们要作一个统计系统的管理后台,咱们想知道统计数据的实时状况。相似这种更新频繁、低延迟的场景,SSE 能够彻底知足。服务器

其余一些应用场景:例如邮箱服务的新邮件提醒,微博的新消息推送、管理后台的一些操做实时同步等,SSE 都是不错的选择。网络

SSE vs. WebSocket

SSE 是单向通道,只能服务器向客户端发送消息,若是客户端须要向服务器发送消息,则须要一个新的 HTTP 请求。这对比 WebSocket 的双工通道来讲,会有更大的开销。这么一来的话就会存在一个「何时才须要关心这个差别?」的问题,若是平均每秒会向服务器发送一次消息的话,那应该选择 WebSocket。若是一分钟仅 5 - 6 次的话,其实这个差别并不大。socket

在浏览器兼容方面,二者差很少。在较早以前,每当须要创建双向 Socket 时就会使用 Flash,在移动浏览器不支持 Flash 的状况下,WebSocket 的兼容是比较难作的。ide

SSE 我认为最大的优点是便利:ui

  • 实现一个完整的服务仅须要少许的代码;
  • 能够在现有的服务中使用,不须要启动一个新的服务;
  • 能够用任何一种服务端语言中使用;
  • 基于 HTTP/HTTPS 协议,能够直接运行于现有的代理服务器和认证技术。

有了这些优点,在选择使用 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 中继承了属性和方法,其内置了 3EventHandler 属性、2 个只读属性和 1 个方法:

EventHandler 属性

EventSource.onopen 在链接打开时被调用。

EventSource.onmessage 在收到一个没有 event 属性的消息时被调用。

EventSource.onerror 在链接异常时被调用。

只读属性

EventSource.readyState 一个 unsigned short 值,表明链接状态。可能值是CONNECTING (0), OPEN (1), 或者 CLOSED (2)。

EventSource.url 链接的 URL。

方法

EventSource.close() 关闭链接。

SSE 如何保证数据完整性

客户端在每次接收到消息时,会把消息的 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();

浏览器兼容

EventSource 浏览器兼容
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许可证) 转载请注明出处

相关文章
相关标签/搜索