服务器事件推送(Server Sent Events)协议讲解

提到 Web 消息推送,几乎所有的人的第一反应都是 websocket。诚然 websocket 技术的确是页面与服务器之间通信的首选,其有很多优点,比如双全工通信,支持二进制传输等等。

但是,你真的需要这样的特性么?很多时候,我们的需求只是一个单向消息推送,有必要上 websocket 技术吗?虽然 websocket 借助了 http 协议完成握手,但它毕竟不是 http,你的服务器架构需要做调整,你需要增加 websocket 服务器,这其实对你来说也是一种负担。

因为最近有一个简单的消息推送服务,我参考了网络上的所有资料,发现一个问题:大家几乎对“服务器事件推送”这个 HTML5 新增标准没什么了解。在查阅了部分资料以及项目实践之后,我总结出这篇文章,希望能对你有所帮助。

 1. 什么是服务器消息推送(SSE)?

简单来说,SSE 就是由服务端发起的数据推送行为。在 websocket 之前,我们如果要想实现服务器消息推送,就需要实现“订阅/发布”模型,具体实现有两种:

  1. 客户端发起轮询,定时获取最新数据。这种方式会造成大量的网络资源浪费,而且因为存在轮询间隔,实时性不高。
  2. 客户端发起请求,服务端不立即回复响应,而是保持这个会话,等到需要推送消息了,再回复响应。这种方式能保证实时性,但是因为受限 nginx 等反代服务,基本都会因为会话超时导致链接断开,此时客户端需要根据情况判断发起重连。

而到了现在,我们几乎不再使用这两种实现方式了,新的消息推送机制除了大家都知道的 websocket 之外就是我们今天要讲的 SSE,在 HTML5 中的实现就是 EventSource

EventSource 是服务器推送的一个网络事件接口。一个 EventSource 实例会对 HTTP 服务开启一个持久化的连接,以 text/event-stream 格式发送事件,会一直保持开启直到被要求关闭。

一旦连接开启,来自服务端传入的消息会以事件的形式分发至客户端中。如果接收消息中有一个事件字段,触发的事件与事件字段的值相同。如果没有事件字段存在,则将触发通用事件。

与 websocket 不同的是,SSE 是单向的。数据信息被单向从服务端到客户端分发。当你不需要从客户端发送数据到服务器时,它可能就是你的绝佳选择。

与 websocket 相比,SSE 有独特的优势:

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

2. Event-Stream(SSE消息事件流)

event-stream 就是一个以 UTF-8 编码的简单文本数据流。event-stream 中的消息之间由“\n\n”分隔。

规范中定义了以下字段:

  • event:事件的类型。它允许你对不同的内容使用相同的流。客户端可以决定只监听一种类型的事件,或者对每种事件类型进行不同的解释。
  • data:事件的数据。你可以放置多行数据。
  • id:每个事件流的 id。浏览器会一直跟踪最近的事件 ID,如果发生了重连,浏览器会把最近接收到的事件 ID 放入 HTTP Header “Last-Event-ID” 中,作为一种简单的同步机制。
  • retry:当连接中断时,浏览器用来尝试重新连接的等待时间。重新连接过程是自动的,默认设置为3秒。在重新连接的过程中,最后接收到的 id 会自动发送给服务器。如果你是使用 websocket 或者长连接轮询,你需要自己编写重连代码。
  • “:”:如果以冒号开头,则会被认为是注释。

下面是一些例子:

data: hello word\n\n

data: first line\n data: second line\n\n

id: 12345 event: newuser data: {user name : JohnDoe}\n data: {user name : JohnDoe2}\n\n

: And yes, you can easily send JSON format without breaking the synthax! And that is a very very good news!

id: 12345 event: newuser data: { \n data: first name : John, \n data: last name : Doe \n data: }\n\n

3. 服务器实现

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

几乎所有的编程语言都可以实现 SSE,下面是 flask 下的实现。

@app.route('/listen', methods=['GET'])
def listen():

    def stream():
        while True:
            yield 'data: %s\n\n' % str(now())
            sleep(5)

    return flask.Response(stream(), mimetype='text/event-stream')

4. 浏览器实现

由于 SSE 是 W3C 标准定义的一个基于 web 的 API,所以 web 客户端的实现就十分简单直接。

下面是一个例子,它实现了连接、消息处理:

var source = new EventSource('/sse');

source.onopen = function() {
   connectionOpen(true);
};
source.onerror = function () {
  connectionOpen(false);
};

source.addEventListener('connections', updateConnections, false);
source.addEventListener('requests', updateRequests, false);
source.addEventListener('uptime', updateUptime, false);

source.onmessage = function (event) {
  // a message without a type was fired
};

代码是不是很简单?当然,唯一的问题就是兼容性不好,如果浏览器不支持,你需要添加 polyfill。

5. 结论

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