服务器推送事件(Server-sent Events,简称SSE
,下同)是HTML5规范中的一个组成部分,可以用来从服务端实时推送数据到浏览器端。相对于WebSocket技术来说,SSE只是单向通信(只能实现服务器向浏览器推送消息,而浏览器不能通过sse向服务器主动发送消息),使用起来也更加简单,对服务器端的改动也比较小,特别适合于诸如监控数据、消息推送等应用场景。
SSE简述
Server-sent Events比较简单,主要由两个部分组成:
- 第一个部分是服务器端与浏览器端之间的
通讯协议
(基于纯文本); - 第二部分则是在浏览器端可供 JavaScript 使用的
EventSource
对象。
通信协议
这里详细介绍SSE的通信协议。
- SSE的通讯协议是基于
纯文本
的简单协议,即服务端和浏览器之间采用纯文本进行通信。 - 服务器端响应的头部信息(内容类型)
Content-Type
必须是text/event-stream
。 - 响应文本的内容可以看成是一个事件流(
Event stream
),由不同的事件所组成。 - 事件流(
Event stream
)强制使用UTF8编码,且无法修改编码方式; - 每个事件由
类型(event)
和数据(data)
两部分组成,同时每个事件可以有一个可选的标识符(id)
。 - 事件流中每行的结尾可以是
CRLF
、LF
、CR
三者中的任意一个。(CRLF
是Carriage-Return Line-Feed的缩写,意思是回车换行,就是回车(CR
, ASCII 13,\r
) 与换行(LF
, ASCII 10,\n
)) - 每个事件的数据可能由多行组成,每个事件之间通过额外的空行(
CRLF
、LF
、CR
三者中的任意一个)来分隔。 - 对于每一行来说,冒号(
:
)前面表示的是该行的类型,冒号后面则是对应的值(可以为空)。其事件类型如下:
事件类型
SSE的事件类型可分为五类。
类型为 空白,表示该行是注释,会在处理时被忽略。举例如下:
- 空白注释
:
- 带描述的注释
:this is a commont
- 空白注释
类型为 data,表示该行包含的是数据。以data开头的行可以连续出现多次,所有这些行都是该事件的数据。多行data最终的数据每行与每行中间都有一个
\n
,但最后没有\n
。举例如下:- 单行data,最终data为:”sse event”
data:sse event
- 多行data,最终data为:”AAAA\nBBBB”
data: AAAA
data:BBBB
- 单行data,最终data为:”sse event”
类型为 event,表示该行用来声明事件的类型。浏览器在收到数据时,会产生对应类型的事件。举例如下:
- 自定义myevnet,可触发
source.addEventListener('myevent', (event) => {console.log(event.data)})
。其中,event.data==='my event data\ncontinue'
event: myevent
data: my event data
data:continue
- 自定义myevnet,可触发
类型为 id,表示该行用来声明事件的标识符(整数字符串)。标识符id主要用在尝试重连的请求头
Last-Event-ID
字段中,给服务器提供信息。举例如下:- 指定标识符id,如果此时断开连接,下次重连时的请求头中会自动将’30’放在
Last-Event-ID
字段中,服务器可以根据这个请求头字段做特定的处理。id: 30
data: dddd
- 指定标识符id,如果此时断开连接,下次重连时的请求头中会自动将’30’放在
类型为 retry,表示该行用来声明浏览器在连接断开之后进行再次连接之前的等待时间(毫秒数),服务器可以动态调节推送频率。
- 指定下次重连时间为3000ms,可动态变化
retry: 3000
- 指定下次重连时间为3000ms,可动态变化
事件数据
服务器端响应内容的示例
|
|
相关HTTP header
|
|
特别注意
- Event stream请求可以通过HTTP状态码301和307进行重定向;
- 当连接关闭时,浏览器会尝试自动重连,除非收到HTTP状态吗204(No Content);
- Event stream中,冒号(
:
)可以在行首表示改行是注释; - Event stream中,非注释行冒号(
:
)后可以有一个空格,该空格不会计入data buffer中; - Event stream中,最后一行如果没有额外的空行,会导致最后一个事件推送不成功;
- 对于代理服务器,因其在特定情况下会在短暂的延时后断开HTTP连接,设计者可以考虑每隔15s推送一条注释消息;
- 在SSE重连时,浏览器端会找到上一个合法的id,跳过那些没有设置id的事件,并通过Last-Event-ID请求头字段发送给服务器;如果上一条id为空,则表示清空last event ID string,这种情况下,浏览器重连时并不会发送Last-Event-ID请求头字段。
Event Source对象
Event Source对象有4个要点,其中后3个都不暴露在Event Source对象上:
- url;
- 请求;
- 重连事件;
- last event ID string;
Event Source对象有如下API接口:
- url (read-only);
- withCredentials (read-only);
- readyState // CONNECTING (0), OPEN (1), CLOSED (2)
- EventHandler // onopen, onmessage, onerror
- close (void)
具体实现方案
Browser端
目前,除IE外,几乎所有的浏览器都支持sse,即window下有EventSource属性(对象)。对于IE可以使用简易轮询或COMET
技术来实现,也可以使用polyfill。
下图是我在Can I use上针对SSE于2017年10月26日的查询结果:
- 具体实现
|
|
Server端
PHP实现
1. 简单版
鄙人常用的后台语言是PHP,网上对PHP实现SSE的大部分实现方法如下(来自w3cshool)。
|
|
经测试,上述简单版的PHP代码虽然能实现推送的功能,但这种写法实际上的效果其实跟轮询差不多。之所以这么讲,是因为从chrome开发调试工具的Network里可以看到间歇性的多条类型为eventsource的请求,如下图示。
这是因为浏览器上的EventSource实例默认会自动重连。上述PHP代码实际上只是一个HTTP短连接,只提供一次简短的sse推送。正是由于自动重连的存在,所以每次短暂的sse推送之后,每隔一段时间便会有一次重连再获取一次新的简短的sse推送。
2. while(true)版
另一种实现方法是while(true)
的写法,虽然又会造成服务器端资源的浪费(例如HTTP长连接等)。
|
|
3. 终极版
改进的方法是定时或定次(推送事件的次数)服务器端主动断开HTTP长连接,并根据每次推送事件的id(last-evnet-id)来在再次重连的时候恢复推送记录。
|
|
nodejs实现
这里使用node自带的http库来实现,简单添加了允许所有域跨域请求的请求头,随机推送一个事件,并且会定次(100次)断开长连接,并能根据last event id恢复。
|
|
以上。
参考资料
- HTML Specification(强烈建议仔细研读)
- EventSource polyfill(支持IE的polyfill)
- HTML5 服务器推送事件(Server-sent Events)实战开发