Server-Sent Events 的协议细节和实现

3,667 阅读4分钟
原文链接: zhuanlan.zhihu.com

前言

同为浏览器推送技术,相较于 WebSocket 而言,Server-Sent Events (简称SSE)更少被人知晓,具体实践也较少。

原因有两点:

  • WebSocket 比 SSE 更强大,Websocket 在客户端和服务器之间建立了双向的实时通信。而 SSE 只支持从服务器到客户端的单向实时通信。
  • WebSocket 在浏览器方面支持更广(详见下图),IE / Edge 几乎根本不支持 SSE

然而,就第一点而言,与 WebSocket 相比,SSE 也有独特的优势。

  • SSE 的浏览器端实现内置断线重连和消息追踪的功能,WebSocket 也能实现,但是不在协议设计范围内,需要手动处理。
  • SSE 实现简单,完全复用现有的 HTTP 协议,而 WebSocket 是相对独立于 HTTP 的一套标准,跨平台实现较为复杂。

协议实现

SSE 协议很简单,本质上是一个客户端发起的 HTTP Get 请求,服务器在接到该请求后,返回 200 OK 状态,同时附带以下 Headers

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
  • SSE 的 MIME Type 规定为 text/event-stream
  • SSE 肯定不允许缓存
  • SSE 是一个一直打开的 TCP 连接,所以 Connection 为 Keep-Alive

协议实现

SSE 协议很简单,本质上是一个客户端发起的 HTTP Get 请求,服务器在接到该请求后,返回 200 OK 状态,同时附带以下 Headers

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
  • SSE 的 MIME Type 规定为 text/event-stream
  • SSE 肯定不允许缓存
  • SSE 是一个一直打开的 TCP 连接,所以 Connection 为 Keep-Alive

之后,服务器保持连接,在 Body 中持续发送文本流,以实现实时消息推送。

基础格式

文本流基础格式如下,以行为单位的,以冒号分割 Field 和 Value,每行结尾为 \n,每行会Trim掉前后空字符,因此 \r\n 也可以。

field: value\n

注释以冒号打头,格式如下

: This is a comment\n

事件

事件之间用 额外的\n 隔断, 每个事件既可以为单行,也可为多行。

下面所示是两个由单行组成的事件

data:  message\n\n
data:  message2\n\n

而这一个是由多行组成的一个事件,更加易读

data: {\n
data: "foo": "bar",\n
data: "baz", 555\n
data: }\n\n

事件唯一标示

每一个事件可以指定 ID

id: msg1\n
data: message\n\n

浏览器会一直跟踪最近的事件ID,如果发生了重连,浏览器会把最近接收到的事件ID放入 HTTP Header “Last-Event-ID” 中,作为一种简单的同步机制。

命名事件

除了 ID 唯一标示一个事件之外,也可以通过命名的方式,区分一组类型的事件。默认情况下,事件会被命名为 “message”。

event: foo\n
data: a foo event\n\n
data: an unnamed event\n\n
data: a bar event\n
event: bar\n\n

上面的例子实际上是三个事件,第一个事件命名为 “foo”,第二个事件没有命名,第三个事件命名为”bar”。可以看出,在一个事件内部,”event” 可以放在前面,也可以放在末尾。

重连时间

一般情况下,连接中断的时候,客户端会在 3 秒内进行重连,这个时间也可以由服务器来指定

retry: 10000\n

服务器实现

要在服务器端实现 SSE 必须要注意,SSE 为每个用户保持了一个 TCP 连接,这就意味着Apache 之类的基于 线程/进程 的服务器引擎不适合这个工作。

而 Node.js 绝对是最佳人选。

具体示例可以参考这篇文章 Server-Sent Events in Node.js

浏览器调用

检测SSE支持

一般可以通过检测 EventSource 对象是否存在来判定当前浏览器是否支持 SSE

function supportsSSE() {
  return !!window.EventSource;
}

连接事件源

直接创建 EventSource 对象即可,创建完成后,浏览器会及时打开。

new EventSource(url);

事件源连接后会发送 “open” 事件,可以用两种方式监听

source.onopen = function(event) {
  // handle open event
};
source.addEventListener("open", function(event) {
  // handle open event
}, false);

接收事件

和上面类似,有两种方式可以接收事件。浏览器会自动把一个消息中的多个分段拼接成一个完整的字符串,因此,可以轻松地在这里使用 JSON 序列化和反序列化处理。

source.onmessage = function(event) {
  var data = event.data;
  var origin = event.origin;
  var lastEventId = event.lastEventId;
  // handle message
};
source.addEventListener("message", function(event) { 
  var data = event.data;
  var origin = event.origin;
  var lastEventId = event.lastEventId;
  // handle message }, false);

命名事件

命名事件不会由 “message” 监听触发,而是使用独立的监听

source.addEventListener("foo", function(event) {
  var data = event.data;
  var origin = event.origin;
  var lastEventId = event.lastEventId;
  // handle message
}, false);

错误处理

source.onerror = function(event) {
  // handle error event
};
source.addEventListener("error", function(event) {
  // handle error event
}, false);

主动断开连接

source.close();

连接状态

switch (source.readyState) {
  case EventSource.CONNECTING:
    // do something
    break;
  case EventSource.OPEN:
    // do something
    break;
  case EventSource.CLOSED:
    // do something
    break;
  default:
    // this never happens
    break;
}

结论

综合而言,相较于 WebSocket,SSE 基于 HTTP 协议单向工作,更加简单,易用。在一些情况下,使用 SSE 反而是更好的选择。