什么是Server-Sent Events

从名字可以看出,服务器发送事件(Server-Sent Events)是一种服务端向客户端推送消息的方法。服务器发送事件规范(Server-Sent Event)描述了一个内置的类EventSource,它可以用来与服务器保持链接并且可以从服务器接收时间。

WebSocket类似,这个链接是持久的,但是两者有一些不同:

WebSocketEventSource
通信方向双向:客户和服务器都可以交换信息单向:只有服务器能发送数据
消息类型可以发送文本以及二进制消息只能发送文本消息
网络协议WebSocket协议常规HTTP协议

从上表可以看出,Server-Sent Event(以下简称SSE)相比WebSocket是一种更轻量的消息推送方式。SSE使用常规HTTP协议,并且支持断线重连 ,无需额外实现。SSE使用相对来说更简单——在服务器端,只需要按照一定格式返回消息;在客户端中,只需要为一些事件类型绑定监听函数,和处理其他普通的事件没多大区别。

客户端接收消息

创建对象

客户端的API就是文章开头提到的EventSource接口,要接受消息,我们只需要创建一个EventSource对象即可,并在创建对象的同时指定接受消息的来源URI,例如:

const evtSource = new EventSource(url);

这个url也可以跨域,跨域时可以在选项中指定withCredentials属性,表示是否一起发送凭证,例如:

const evtSource = new EventSource("//api.example.com/message", { withCredentials: true } );

监听消息

成功初始化之后,就可以添加onmessage事件处理函数开始监听从服务器发出的消息了:

evtSource.onmessage = function(event) {
  console.log("New message", event.data);
  // handle message
};

当然也支持另外一种写法:

evtSource.addEventListener('message', function (event) {
  console.log("New message", event.data);
  // handle message
}, false);

错误处理

如果发生错误(如连接中断或者跨域错误),就会触发onerror事件:

evtSource.onerror = function (event) {
  // handle error event
};

同样,也支持另外一种写法:

evtSource.addEventListener('error', function (event) {
  // handle error event
}, false);

重连

在发生错误断开连接之后,连接会自动重连,而无需额外处理。如果需要指定自动重连的延迟时间,服务器端可以在返回的消息中指定。

retry: 15000

浏览器会等待指定的时间以后开始重连,也有可能更长,比如浏览器知道当前没有网络的情况下,会等待知道有网络之后再重连。

关闭连接

如果客户端要关闭连接,直接调用对应的方法即可:

evtSource.close();

如果服务端想拒绝客户端重连,只需返回HTTP状态码204即可。

备注:当连接关闭以后,是没有办法重新打开这个连接的。如果需要重连,只能重新创建一个连接。

事件流数据格式

服务器要向客户端发送SSE数据,必须发送以下HTTP头信息:

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

事件流只是一个简单的文本数据流,使用UTF8编码。每条消息(message)后面都有一条空行作为分隔符(两个换行符:\n\n)。每条消息由多行组成,每一行由一个字段名,一个冒号以及字段值组成。

field: value

其中字段名field只能取以下四种:

event

事件类型。如果指定了该字段,则在客户端接收到该条消息时,会在当前的EventSource对象上触发一个事件,事件类型就是该字段的值,也称为命名事件。你可以使用addEventListener()方法在当前EventSource对象上监听任意类型的命名事件,如果该条消息没有event字段,则会触发onmessage属性上的事件处理函数.

data

消息的数据字段。如果一条消息包含多个data字段,客户端会用换行符把它们连接成一个字符串来作为字段值。

id

事件 ID,会成为当前EventSource对象的内部属性“最后一个事件 ID”(Last-Event-ID)的属性值。

retry

一个整数值,指定了重连的时间间隔(单位为毫秒),必须为整数。

有一个例外是,消息可以以冒号开头。以冒号开头的消息会理解为注释,被客户端忽略。在没有消息时,通常会间隔一段时间发送一条注释来保持连接。

一个事件流示例:

event: userconnect
data: {"username": "bobby", "time": "02:33:48"}

data: Here's a system message of some kind that will get used
data: to accomplish some task.

event: usermessage
data: {"username": "bobby", "time": "02:34:11", "text": "Hi everyone."}

服务端实现

一个简单的node实现如下:

let http = require('http');
let static = require('node-static');
let fileServer = new static.Server('.');

function onDigits(req, res) {
  // 输出固定的header
  res.writeHead(200, {
    'Content-Type': 'text/event-stream; charset=utf-8',
    'Cache-Control': 'no-cache'
  });

  let i = 0;

  let timer = setInterval(write, 1000);
  write();

  function write() {
    i++;

    // 四条消息后会断开
    if (i == 4) {
      res.write('event: bye\ndata: bye-bye\n\n');
      clearInterval(timer);
      res.end();
      return;
    }
    res.write('data: ' + i + '\n\n');
  }
}

function accept(req, res) {
  if (req.url == '/digits') {
    onDigits(req, res);
    return;
  }

  fileServer.serve(req, res);
}

if (!module.parent) {
  http.createServer(accept).listen(8080);
} else {
  exports.accept = accept;
}

客户端实现如下:

<!DOCTYPE html>
<script>
let eventSource;

function start() {
  if (!window.EventSource) {
    // IE or an old browser
    alert("The browser doesn't support EventSource.");
    return;
  }

  eventSource = new EventSource('digits');

  eventSource.onopen = function(e) {
    console.log(e);
    log("Event: open");
  };

  eventSource.onerror = function(e) {
    log("Event: error");
    if (this.readyState == EventSource.CONNECTING) {
      log(`Reconnecting (readyState=${this.readyState})...`);
    } else {
      log("Error has occured.");
    }
  };

  eventSource.addEventListener('bye', function(e) {
    log("Event: bye, data: " + e.data);
  });

  eventSource.onmessage = function(e) {
    log("Event: message, data: " + e.data);
  };
}

function stop() { // when "Stop" button pressed
  eventSource.close();
  log("eventSource.close()");
}

function log(msg) {
  logElem.innerHTML += msg + "<br>";
  document.documentElement.scrollTop = 99999999;
}
</script>

<button onclick="start()">Start</button> Press the "Start" to begin.
<div id="logElem" style="margin: 6px 0"></div>

<button onclick="stop()">Stop</button> "Stop" to finish.

参考链接

  1. Using server-sent events:https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
  2. MDN EventSource:https://developer.mozilla.org/en-US/docs/Web/API/EventSource
  3. Browser APIs and Protocols: Server-Sent Events (SSE):https://hpbn.co/server-sent-events-sse/
  4. Server Sent Events:https://javascript.info/server-sent-events

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.