SSE技术详解:使用HTTP做服务端数据推送应用的技术
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
SSE技术详解:使⽤HTTP做服务端数据推送应⽤的技术
SSE ( Server-sent Events )是 WebSocket 的⼀种轻量代替⽅案,使⽤ HTTP 协议。
严格地说,HTTP 协议是没有办法做服务器推送的,但是当服务器向客户端声明接下来要发送流信息时,客户端就会保持连接打
开,SSE 使⽤的就是这种原理。
⼀、SSE 能做什么?
理论上, SSE 和 WebSocket 做的是同⼀件事情。
当你需要⽤新数据局部更新⽹络应⽤时,SSE 可以做到不需要⽤户执⾏任何操作,便可以完成。
举例我们要做⼀个统计系统的管理后台,我们想知道统计数据的实时情况。
类似这种更新频繁、低延迟的场景,SSE 可以完全满⾜。
其他⼀些应⽤场景:例如邮箱服务的新邮件提醒,微博的新消息推送、管理后台的⼀些操作实时同步等,SSE 都是不错的选择。
⼆、SSE vs. WebSocket
SSE 是单向通道,只能服务器向客户端发送消息,如果客户端需要向服务器发送消息,则需要⼀个新的 HTTP 请求。
这对⽐WebSocket 的双⼯通道来说,会有更⼤的开销。
这么⼀来的话就会存在⼀个「什么时候才需要关⼼这个差异?」的问题,如果平均每秒会向服务器发送⼀次消息的话,那应该选择 WebSocket。
如果⼀分钟仅 5 - 6 次的话,其实这个差异并不⼤。
在浏览器兼容⽅⾯,两者差不多。
在较早之前,每当需要建⽴双向 Socket 时就会使⽤ Flash,在移动浏览器不⽀持 Flash 的情况
下,WebSocket 的兼容是⽐较难做的。
SSE 我认为最⼤的优势是便利:
实现⼀个完整的服务仅需要少量的代码;
可以在现有的服务中使⽤,不需要启动⼀个新的服务;
可以⽤任何⼀种服务端语⾔中使⽤;
基于 HTTP / HTTPS 协议,可以直接运⾏于现有的代理服务器和认证技术。
有了这些优势,在选择使⽤ SSE 时就已经为⾃⼰的项⽬节约了不少成本。
三、SSE(Server-sent Events)在HTML 5中的技术规范和定义
Server-sent Events 规范是 HTML 5 规范的⼀个组成部分,具体的规范⽂档见参考资源。
该规范⽐较简单,主要由两个部分组成:
第⼀个部分是服务器端与浏览器端之间的通讯协议,
第⼆部分则是在浏览器端可供 JavaScript 使⽤的 EventSource 对象。
通讯协议是基于纯⽂本的简单协议。
服务器端的响应的内容类型是“text/event-stream”。
响应⽂本的内容可以看成是⼀个事件流,由不同的事件所组成。
每个事件由类型和数据两部分组成,同时每个事件可以有⼀个可选的标识符。
不同事件的内容之间通过仅包含回车符和换⾏符的空⾏(“\r\n”)来分隔。
每个事件的数据可能由多⾏组成。
下⾯代码给出了服务器端响应的⽰例:
data: first event
data: second event
id: 100
event: myevent
data: third event
id: 101
: this is a comment
data: fourth event
data: fourth event continue
如上所⽰,每个事件之间通过空⾏来分隔。
对于每⼀⾏来说,冒号(“:”)前⾯表⽰的是该⾏的类型,冒号后⾯则是对应的值。
可能的类型包括:
类型为空⽩,表⽰该⾏是注释,会在处理时被忽略。
类型为 data,表⽰该⾏包含的是数据。
以 data 开头的⾏可以出现多次。
所有这些⾏都是该事件的数据。
类型为 event,表⽰该⾏⽤来声明事件的类型。
浏览器在收到数据时,会产⽣对应类型的事件。
类型为 id,表⽰该⾏⽤来声明事件的标识符。
类型为 retry,表⽰该⾏⽤来声明浏览器在连接断开之后进⾏再次连接之前的等待时间。
在上述代码中,第⼀个事件只包含数据“first event”,会产⽣默认的事件;第⼆个事件的标识符是 100,数据为“second event”;第三个事件会产⽣类型为“myevent”的事件;最后⼀个事件的数据为“fourth event\nfourth event continue”。
当有多⾏数据时,实际的数据由每⾏数据以换⾏符连接⽽成。
如果服务器端返回的数据中包含了事件的标识符,浏览器会记录最近⼀次接收到的事件的标识符。
如果与服务器端的连接中断,当浏览器端再次进⾏连接时,会通过 HTTP 头“Last-Event-ID”来声明最后⼀次接收到的事件的标识符。
服务器端可以通过浏览器端发送的事件标识符来确定从哪个事件开始来继续连接。
对于服务器端返回的响应,浏览器端需要在 JavaScript 中使⽤ EventSource 对象来进⾏处理。
EventSource 使⽤的是标准的事件监听器⽅式,只需要在对象上添加相应的事件处理⽅法即可。
EventSource 提供了三个标准事件
如之前所述,服务器端可以返回⾃定义类型的事件。
对于这些事件,可以使⽤ addEventListener ⽅法来添加相应的事件处理⽅法。
如下代码给出了 EventSource 对象的使⽤⽰例。
var es = new EventSource('events');
es.onmessage = function(e) {
console.log(e.data);
};
es.addEventListener('myevent', function(e) {
console.log(e.data);
});
在指定 URL 创建出 EventSource 对象之后,可以通过 onmessage 和 addEventListener ⽅法来添加事件处理⽅法。
当服务器端有新的事件产⽣,相应的事件处理⽅法会被调⽤。
EventSource 对象的 onmessage 属性的作⽤类似于 addEventListener( ‘ message ’ ),不过onmessage 属性只⽀持⼀个事件处理⽅法。
四、简单⽰例
下⾯是⼀个简单的⽰例,实现⼀个 SSE 服务。
1、服务端
'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 个字符,但是发送到客户端上的时候不会触发任何事件,这对客户端来说是⾮常友好的。
所以注释⼀般被⽤于维持服务器和客户端的长连接。
效果:
2、客户端
我们创建了⼀个 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从⽗接⼝中继承了属性和⽅法,其内置了 3 个属性、2 个只读属性和 1 个⽅法:
EventHandler 属性
EventSource.onopen 在连接打开时被调⽤。
EventSource.onmessage 在收到⼀个没有 event 属性的消息时被调⽤。
EventSource.onerror 在连接异常时被调⽤。
只读属性
EventSource.readyState ⼀个 unsigned short 值,代表连接状态。
可能值是 CONNECTING (0), OPEN (1), 或者 CLOSED (2)。
EventSource.url 连接的 URL。
⽅法
EventSource.close() 关闭连接
效果:
五、SSE使⽤注意事项
1、SSE 如何保证数据完整性
客户端在每次接收到消息时,会把消息的 id 字段作为内部属性 Last-Event-ID 储存起来。
SSE 默认⽀持断线重连机制,在连接断开时会触发 EventSource 的 error 事件,同时⾃动重连。
再次连接成功时 EventSource 会把Last-Event-ID 属性作为请求头发送给服务器,这样服务器就可以根据这个 Last-Event-ID 作出相应的处理。
这⾥需要注意的是,id 字段不是必须的,服务器有可能不会在消息中带上 id 字段,这样⼦客户端就不会存在 Last-Event-ID 这个属性。
所以为了保证数据可靠,我们需要在每条消息上带上 id 字段。
2、减少开销
在 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();
3、浏览器兼容
向下兼容:早些时候,为了实现数据实时更新最常见的⽅法就是轮询。
轮询是以⼀个固定频率向服务器发送请求,服务器在有数据更新时返回新的数据,以此来管理数据的更新。
这种轮询的⽅式不但开销⼤,⽽且更新的效率和频率有关,也不能达到及时更新的⽬的。
接着便出现了长轮询的⽅式:客户端向服务器发送请求之后,服务器会暂时把请求挂起,等到有数据更新时再返回最新的数据给客户端,客户端在接收到新的消息后再向服务器发送请求。
与常规轮询的不同之处是:数据可以做到实时更新,可以减少不必要的开销。
这⾥有⼀个「选择长轮询还是常规轮询?」的命题,长轮询是不是总⽐常规轮询占有优势?我们可以从带宽占⽤的⾓度分析,如果⼀个程序数据更新太过频繁,假设每秒 2 次更新,如果使⽤长轮询的话每分钟要发送 120 次 HTTP 请求。
如果使⽤常规轮询,每 5 秒发送⼀次请求的话,⼀分钟才 20 次,从这⾥看,常规轮询更占有优势。
长轮询和 SSE 最关键的区别在于,每⼀次数据更新都需要⼀次 HTTP 请求。
和 WebSocket 还有 SSE ⼀样,长轮询也会占⽤⼀个socket。
在数据更新效率上和 SSE 差不多,⼀有数据更新就能检测到。
加上所有浏览器都⽀持,是⼀个不错的 SSE 替代⽅案。
⽂章介绍了 SSE 的⽤法及使⽤过程中的⼀些技巧。
对⽐ WebSocket,SSE 在开发时间和成本上占有较⼤的优势。
做数据推送服务,除了 WebSocket,SSE 也是⼀个不错的选择,希望对⼤家有所帮助。