Server-Sent Events - SSE

服务器向客户端推送数据,有很多解决方案。除了“轮询” 和 WebSocket,HTML 5 还提供了 Server-Sent Events(SSE)。

SSE 的本质

严格地说,HTTP 协议只能客户端向服务器发起请求,无法做到服务器主动向客户端推送信息。但是,有一种特殊情况,就是服务器向客户端声明,接下来要发送的是流信息(streaming)。也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。本质上,这种通信就是以流信息的方式,完成一次用时很长的下载。

SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 HTTP 协议,目前除了 IE/Edge,其他浏览器都支持。

与 WebSocket 的比较

SSE 与 WebSocket 作用相似,都是建立浏览器与服务器之间的通信渠道,然后服务器向浏览器推送信息。一旦创建了初始连接,事件流将保持打开状态,直到客户端关闭。该技术通过传统的 HTTP 发送,并具有 WebSockets 缺乏的各种功能,例如自动重新连接、事件 ID 以及发送任意事件的能力。

总体来说,WebSocket 更强大和灵活。因为它是全双工通道,可以双向通信;SSE 是单向通道,只能服务器向浏览器发送,因为流信息本质上就是下载。如果浏览器向服务器发送信息,就变成了另一次 HTTP 请求。

SSE 也有自己的优点

  • SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议。
  • SSE 属于轻量级,使用简单;WebSocket 协议相对复杂。
  • SSE 默认支持断线重连,WebSocket 需要自己实现。
  • SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据。
  • SSE 支持自定义发送的消息类型。

所以sse适用于更新频繁、低延迟并且数据都是从服务端发到客户端。

浏览器兼容难度比较高, 不过目前基本都支持了。

Can I use Server-sent events Support tables for HTML5, CSS3, etc

服务器向浏览器发送的 SSE 数据,必须是 UTF-8 编码的文本,具有如下的 HTTP 头信息。

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

上面三行之中,第一行的Content-Type必须指定 MIME 类型为event-steam

其他几种基于streaming的方式

基于 Iframe 及 htmlfile 的流(Iframe Streaming)

iframe 流方式是在页面中插入一个隐藏的 iframe,利用其 src 属性在服务器和客户端之间创建一条长链接,服务器向 iframe 传输数据(通常是 HTML,内有负责插入信息的 JavaScript),来实时更新页面。iframe 流方式的优点是浏览器兼容好。

使用 iframe 请求一个长连接有一个很明显的不足之处:IE、Morzilla Firefox 下端的进度栏都会显示加载没有完成,而且 IE 上方的图标会不停的转动,表示加载正在进行。

Google 的天才们使用一个称为 “htmlfile” 的 ActiveX 解决了在 IE 中的加载显示问题,并将这种方法用到了 gmail+gtalk 产品中。Alex Russell 在 “What else is burried down in the depth’s of Google’s amazing JavaScript?”文章中介绍了这种方法。Zeitoun 网站提供的 comet-iframe.tar.gz,封装了一个基于 iframe 和 htmlfile 的 JavaScript comet 对象,支持 IE、Mozilla Firefox 浏览器,可以作为参考。

  • 优点:实现简单,在所有支持 iframe 的浏览器上都可用、客户端一次连接、服务器多次推送。
  • 缺点:无法准确知道连接状态,IE浏览器在 iframe 请求期间,浏览器 title 一直处于加载状态,底部状态栏也显示正在加载,用户体验不好(htmlfile 通过 ActiveXObject 动态写入内存可以解决此问题)。

AJAX multipart streaming(XHR Streaming)

实现思路:浏览器必须支持 multi-part 标志,客户端通过 AJAX 发出请求 Request,服务器保持住这个连接,然后可以通过 HTTP1.1 的 chunked encoding 机制(分块传输编码)不断 push 数据给客户端,直到 timeout 或者手动断开连接。

  • 优点:客户端一次连接,服务器数据可多次推送。
  • 缺点:并非所有的浏览器都支持 multi-part 标志。

Flash Socket(Flash Streaming)

实现思路:在页面中内嵌入一个使用了 Socket 类的 Flash 程序,JavaScript 通过调用此 Flash 程序提供的 Socket 接口与服务器端的 Socket 接口进行通信,JavaScript 通过 Flash Socket 接收到服务器端传送的数据。

  • 优点:实现真正的即时通信,而不是伪即时。
  • 缺点:客户端必须安装 Flash 插件;非 HTTP 协议,无法自动穿越防火墙。

SSE实现

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

1
2
3
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 也可以。

1
field: value\n

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

1
: This is a comment\n

事件

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

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

1
2
data:  message\n\n
data: message2\n\n

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

1
2
3
4
data: {\n
data: "foo": "bar",\n
data: "baz", 555\n
data: }\n\n

事件唯一标示

每一个事件可以指定 ID

1
2
id: msg1\n
data: message\n\n

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

命名事件

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

1
2
3
4
5
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 秒内进行重连,这个时间也可以由服务器来指定

1
retry: 10000\n

服务器实现

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

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

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

浏览器调用

检测SSE支持

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

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

连接事件源

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

1
new EventSource(url);

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

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

接收事件

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

1
2
3
4
5
6
source.onmessage = function(event) {
var data = event.data;
var origin = event.origin;
var lastEventId = event.lastEventId;
// handle message
};
1
2
3
4
5
source.addEventListener("message", function(event) { 
var data = event.data;
var origin = event.origin;
var lastEventId = event.lastEventId;
// handle message }, false);

命名事件

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

1
2
3
4
5
6
source.addEventListener("foo", function(event) {
var data = event.data;
var origin = event.origin;
var lastEventId = event.lastEventId;
// handle message
}, false);

错误处理

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

主动断开连接

1
source.close();

连接状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 反而是更好的选择。