SSE ( Server-sent Events )是 WebSocket 的一种轻量代替方案,使用 HTTP 协议。
严格地说,HTTP 协议是没有办法做服务器推送的,但是当服务器向客户端声明接下来要发送流信息时,客户端就会保持连接打开,SSE 使用的就是这种原理。
理论上, SSE 和 WebSocket 做的是同一件事情。当你需要用新数据局部更新网络应用时,SSE 可以做到不需要用户执行任何操作,便可以完成。
举例我们要做一个统计系统的管理后台,我们想知道统计数据的实时情况。类似这种更新频繁、 低延迟的场景,SSE 可以完全满足。
其他一些应用场景:例如邮箱服务的新邮件提醒,微博的新消息推送、管理后台的一些操作 实时同步等,SSE 都是不错的选择。
SSE 是单向通道,只能服务器向客户端发送消息,如果客户端需要向服务器发送消息,则需要一个新的 HTTP 请求。 这对比 WebSocket 的双工通道来说,会有更大的开销。这么一来的话就会存在一个「什么时候才需要关心这个差异?」的问题,如果平均每秒会向服务器发送一次消息的话,那应该选择 WebSocket。如果一分钟仅 5 - 6 次的话,其实这个差异并不大。
在浏览器兼容方面,两者差不多。在较早之前,每当需要建立双向 Socket 时就会使用 Flash,在 移动浏览器不支持 Flash 的情况下,WebSocket 的兼容是比较难做的。
SSE 我认为最大的优势是便利:
有了这些优势,在选择使用 SSE 时就已经为自己的项目节约了不少成本。
下面是一个简单的示例,实现一个 SSE 服务。
'use strict';
const http = require('http');
http.createServer((req, res) => {
// 服务器声明接下来发送的是事件流
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
});
// 发送消息
setInterval(() => {
res.write('event: slide\n'); // 事件类型
res.write(`id: ${+new Date()}\n`); // 消息 ID
res.write('data: 7\n'); // 消息数据
res.write('retry: 10000\n'); // 重连时间
res.write('\n\n'); // 消息结束
}, 3000);
// 发送注释保持长连接
setInterval(() => {
res.write(': \n\n');
}, 12000);
}).listen(2000);
服务器首先向客户端声明接下来发送的是事件流( text/event-stream )类型的数据,然后就可以向客户端多次发送消息。
事件流是一个简单的文本流,仅支持 UTF-8 格式的编码。每条消息以一个空行作为分隔符。
在规范中为消息定义了 4 个字段:
event 消息的事件类型。客户端收到消息时,会在当前的 EventSource 对象上触发一个事件,这个事件的名称就是这个字段的值,如果消息没有这个字段,客户端的 EventSource 对象就会触发默认的 message 事件。
id 这条消息的 ID。客户端接收到消息后,会把这个 ID 作为内部属性 Last-Event-ID
,在断开重连 成功后,会把 Last-Event-ID
发送给服务器。
data 消息的数据字段。 客户端会把这个字段解析为字符串,如果一条消息有多个 data 字段,客户端会自动用换行符 连接成一个字符串。
retry 指定客户端重连的时间。只接受整数,单位是毫秒。如果这个值不是整数则会被自动忽略。
一个很有意思的地方是,规范中规定以冒号开头的消息都会被当作注释,一条普通的注释(:\n\n
)对于服务器来说只占 5 个字符,但是发送到客户端上的时候不会触发任何事件,这对客户端来说是非常友好的。所以注释一般被用于维持服务器和客户端的长连接。
我们创建了一个 EventSource
对象,传入参数:url
。并且根据服务器的状态和发送的信息作出响应。
'use strict';
if (window.EventSource) {
// 创建 EventSource 对象连接服务器
const source = new EventSource('http://localhost:2000');
// 连接成功后会触发 open 事件
source.addEventListener('open', () => {
console.log('Connected');
}, false);
// 服务器发送信息到客户端时,如果没有 event 字段,默认会触发 message 事件
source.addEventListener('message', e => {
console.log(`data: ${e.data}`);
}, false);
// 自定义 EventHandler,在收到 event 字段为 slide 的消息时触发
source.addEventListener('slide', e => {
console.log(`data: ${e.data}`); // => data: 7
}, false);
// 连接异常时会触发 error 事件并自动重连
source.addEventListener('error', e => {
if (e.target.readyState === EventSource.CLOSED) {
console.log('Disconnected');
} else if (e.target.readyState === EventSource.CONNECTING) {
console.log('Connecting...');
}
}, false);
} else {
console.error('Your browser doesn\'t support SSE');
}
EventSource从父接口 EventTarget 中继承了属性和方法,其内置了 3 个 EventHandler 属性、2 个只读属性和 1 个方法:
EventSource.onopen 在连接打开时被调用。
EventSource.onmessage 在收到一个没有 event 属性的消息时被调用。
EventSource.onerror 在连接异常时被调用。
EventSource.readyState 一个 unsigned short 值,代表连接状态。可能值是 CONNECTING (0), OPEN (1), 或者 CLOSED (2)。
EventSource.url 连接的 URL。
EventSource.close() 关闭连接。
客户端在每次接收到消息时,会把消息的 id 字段作为内部属性 Last-Event-ID
储存起来。
SSE 默认支持断线重连机制,在连接断开时会 触发 EventSource 的 error 事件,同时自动重连。再次连接成功时 EventSource 会把 Last-Event-ID
属性作为请求头发送给服务器,这样服务器就可以根据这个 Last-Event-ID
作出相应的处理。
这里需要注意的是,id 字段不是必须的,服务器有可能不会在消息中带上 id 字段,这样子客户端就不会存在 Last-Event-ID
这个属性。所以为了保证数据可靠,我们需要在每条消息上带上 id 字段。
在 SSE 的草案中提到,"text/event-stream" 的 MIME 类型传输应当在静置 15 秒后自动断开。在实际的项目中也会有这个机制,但是断开的时间没有被列入标准中。
为了减少服务器的开销,我们也可以有目的的断开和重连。
简单的办法是服务器发送一个 关闭消息并指定一个重连的时间戳,客户端在触发关闭事件时关闭当前连接并创建 一个计时器,在重连时把计时器销毁 。
'use strict';
function connectSSE() {
if (window.EventSource) {
const source = new EventSource('http://localhost:2000');
let reconnectTimeout;
source.addEventListener('open', () => {
console.log('Connected');
clearTimeout(reconnectTimeout);
}, false);
source.addEventListener('pause', e => {
source.close();
const reconnectTime = +e.data;
const currentTime = +new Date();
reconnectTimeout = setTimeout(() => {
connectSSE();
}, reconnectTime - currentTime);
}, false);
} else {
console.error('Your browser doesn\'t support SSE');
}
}
connectSSE();
Broswer support of EventSource from Can I Use...
早些时候,为了实现数据实时更新最常见的方法就是轮询。
轮询是以一个固定频率向服务器发送请求,服务器在有 数据更新时 返回新的数据,以此来管理数据的更新。这种轮询的方式不但开销大,而且更新的效率和频率有关,也不能达到及时更新的目的。
接着便出现了长轮询的方式:客户端向服务器发送请求之后,服务器会暂时把请求挂起,等到有数据更新时再返回最新的数据给客户端,客户端在接收到新的消息后再向服务器发送请求。与常规轮询的不同之处是:数据可以做到实时更新,可以减少不必要的开销。
这里有一个「选择长轮询还是常规轮询?」的命题,长轮询是不是总比常规轮询占有优势?我们可以从带宽占用的角度分析,如果一个程序数据更新太过频繁,假设每秒 2 次更新,如果使用长轮询的话每分钟要发送 120 次 HTTP 请求。如果使用常规轮询,每 5 秒发送一次请求的话, 一分钟才 20 次,从这里看,常规轮询更占有优势。
长轮询和 SSE 最关键的区别在于,每一次数据更新都需要一次 HTTP 请求。和 WebSocket 还有 SSE 一样,长轮询也会 占用一个 socket。在数据更新效率上和 SSE 差不多,一有数据更新就能检测到。加上所有浏览器都支持,是一个不错的 SSE 替代方案。
文章介绍了 SSE 的用法及使用过程中的一些技巧。对比 WebSocket,SSE 在开发时间和成本上占有较大的优势。做数据推送服务,除了 WebSocket,SSE 也是一个不错的选择,希望对大家有所帮助。
Server-Sent Events
EventSource - Web APIs | MDN
Using server-sent events - Web APIs | MDN
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.