什么是Server-Sent Events
从名字可以看出,服务器发送事件(Server-Sent Events)是一种服务端向客户端推送消息的方法。服务器发送事件规范(Server-Sent Event)描述了一个内置的类EventSource
,它可以用来与服务器保持链接并且可以从服务器接收时间。
和WebSocket
类似,这个链接是持久的,但是两者有一些不同:
WebSocket | EventSource | |
通信方向 | 双向:客户和服务器都可以交换信息 | 单向:只有服务器能发送数据 |
消息类型 | 可以发送文本以及二进制消息 | 只能发送文本消息 |
网络协议 | WebSocket协议 | 常规HTTP协议 |
从上表可以看出,Server-Sent Event
(以下简称SSE)相比WebSocket
是一种更轻量的消息推送方式。SSE使用常规HTTP协议,并且支持断线重连 ,无需额外实现。SSE使用相对来说更简单——在服务器端,只需要按照一定格式返回消息;在客户端中,只需要为一些事件类型绑定监听函数,和处理其他普通的事件没多大区别。
客户端接收消息
创建对象
客户端的API就是文章开头提到的EventSource
接口,要接受消息,我们只需要创建一个EventSource
对象即可,并在创建对象的同时指定接受消息的来源URI
,例如:
const evtSource = new EventSource(url);
这个url
也可以跨域,跨域时可以在选项中指定withCredentials
属性,表示是否一起发送凭证,例如:
const evtSource = new EventSource("//api.example.com/message", { withCredentials: true } );
监听消息
成功初始化之后,就可以添加onmessage事件处理函数开始监听从服务器发出的消息了:
evtSource.onmessage = function(event) {
console.log("New message", event.data);
// handle message
};
当然也支持另外一种写法:
evtSource.addEventListener('message', function (event) {
console.log("New message", event.data);
// handle message
}, false);
错误处理
如果发生错误(如连接中断或者跨域错误),就会触发onerror事件:
evtSource.onerror = function (event) {
// handle error event
};
同样,也支持另外一种写法:
evtSource.addEventListener('error', function (event) {
// handle error event
}, false);
重连
在发生错误断开连接之后,连接会自动重连,而无需额外处理。如果需要指定自动重连的延迟时间,服务器端可以在返回的消息中指定。
retry: 15000
浏览器会等待指定的时间以后开始重连,也有可能更长,比如浏览器知道当前没有网络的情况下,会等待知道有网络之后再重连。
关闭连接
如果客户端要关闭连接,直接调用对应的方法即可:
evtSource.close();
如果服务端想拒绝客户端重连,只需返回HTTP状态码204即可。
备注:当连接关闭以后,是没有办法重新打开这个连接的。如果需要重连,只能重新创建一个连接。
事件流数据格式
服务器要向客户端发送SSE数据,必须发送以下HTTP头信息:
Content-Type: text/event-stream
Cache-Control: no-cache
事件流只是一个简单的文本数据流,使用UTF8编码。每条消息(message)后面都有一条空行作为分隔符(两个换行符:\n\n
)。每条消息由多行组成,每一行由一个字段名,一个冒号以及字段值组成。
field: value
其中字段名field只能取以下四种:
event
事件类型。如果指定了该字段,则在客户端接收到该条消息时,会在当前的EventSource
对象上触发一个事件,事件类型就是该字段的值,也称为命名事件。你可以使用addEventListener()
方法在当前EventSource
对象上监听任意类型的命名事件,如果该条消息没有event
字段,则会触发onmessage
属性上的事件处理函数.
data
消息的数据字段。如果一条消息包含多个data
字段,客户端会用换行符把它们连接成一个字符串来作为字段值。
id
事件 ID,会成为当前EventSource
对象的内部属性“最后一个事件 ID”(Last-Event-ID
)的属性值。
retry
一个整数值,指定了重连的时间间隔(单位为毫秒),必须为整数。
有一个例外是,消息可以以冒号开头。以冒号开头的消息会理解为注释,被客户端忽略。在没有消息时,通常会间隔一段时间发送一条注释来保持连接。
一个事件流示例:
event: userconnect
data: {"username": "bobby", "time": "02:33:48"}
data: Here's a system message of some kind that will get used
data: to accomplish some task.
event: usermessage
data: {"username": "bobby", "time": "02:34:11", "text": "Hi everyone."}
服务端实现
一个简单的node实现如下:
let http = require('http');
let static = require('node-static');
let fileServer = new static.Server('.');
function onDigits(req, res) {
// 输出固定的header
res.writeHead(200, {
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache'
});
let i = 0;
let timer = setInterval(write, 1000);
write();
function write() {
i++;
// 四条消息后会断开
if (i == 4) {
res.write('event: bye\ndata: bye-bye\n\n');
clearInterval(timer);
res.end();
return;
}
res.write('data: ' + i + '\n\n');
}
}
function accept(req, res) {
if (req.url == '/digits') {
onDigits(req, res);
return;
}
fileServer.serve(req, res);
}
if (!module.parent) {
http.createServer(accept).listen(8080);
} else {
exports.accept = accept;
}
客户端实现如下:
<!DOCTYPE html>
<script>
let eventSource;
function start() {
if (!window.EventSource) {
// IE or an old browser
alert("The browser doesn't support EventSource.");
return;
}
eventSource = new EventSource('digits');
eventSource.onopen = function(e) {
console.log(e);
log("Event: open");
};
eventSource.onerror = function(e) {
log("Event: error");
if (this.readyState == EventSource.CONNECTING) {
log(`Reconnecting (readyState=${this.readyState})...`);
} else {
log("Error has occured.");
}
};
eventSource.addEventListener('bye', function(e) {
log("Event: bye, data: " + e.data);
});
eventSource.onmessage = function(e) {
log("Event: message, data: " + e.data);
};
}
function stop() { // when "Stop" button pressed
eventSource.close();
log("eventSource.close()");
}
function log(msg) {
logElem.innerHTML += msg + "<br>";
document.documentElement.scrollTop = 99999999;
}
</script>
<button onclick="start()">Start</button> Press the "Start" to begin.
<div id="logElem" style="margin: 6px 0"></div>
<button onclick="stop()">Stop</button> "Stop" to finish.
参考链接
- Using server-sent events:https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
- MDN EventSource:https://developer.mozilla.org/en-US/docs/Web/API/EventSource
- Browser APIs and Protocols: Server-Sent Events (SSE):https://hpbn.co/server-sent-events-sse/
- Server Sent Events:https://javascript.info/server-sent-events