服务器发送的事件 (SSE) 通过 HTTP 连接从服务器向客户端发送自动更新。建立连接后,服务器即可启动数据传输。
您可能需要使用 SSE 从您的 Web 应用发送推送通知。SSE 会单向发送信息,因此您不会收到来自客户端的更新。
大家可能对 SSE 的概念比较熟悉。Web 应用会“订阅”服务器生成的更新流,并且每当发生新事件时,系统都会向客户端发送通知。但是,要真正理解服务器发送的事件,我们需要了解其 AJAX 前身的限制。这包括:
轮询:应用反复向服务器轮询数据。大多数 AJAX 应用都会使用这种技术。使用 HTTP 协议时,数据提取围绕请求和响应格式展开。客户端发出请求,然后等待服务器响应数据。如果没有可用的响应,则返回空响应。额外的轮询会导致更高的 HTTP 开销。
长轮询(挂起 GET / COMET):如果服务器没有可用数据,则服务器会让请求保持打开状态,直到提供新数据。 因此,这种方法通常称为“挂起 GET”。当信息可用时,服务器会进行响应、关闭连接,然后重复该过程。因此,服务器会不断响应新数据。为此,开发者通常会使用一些技巧,例如将脚本标记附加到“无限”iframe。
服务器发送的事件经过彻底设计,非常高效。与 SSE 通信时,服务器可以随时将数据推送到您的应用,而无需发出初始请求。换言之,可以在更新发生时从服务器流式传输到客户端。SSE 在服务器和客户端之间开放了单个单向通道。
服务器发送的事件和长轮询之间的主要区别在于,SSE 由浏览器直接处理,用户只需监听消息即可。
服务器发送的事件与 WebSocket
为什么应选择服务器发送的事件而不是 WebSocket?这个问题问得好。
WebSockets 具有支持双向全双工通信的富协议。双向信道更适合游戏、即时通讯应用以及需要近乎实时的双向更新的任何用例。
但是,有时您只需从服务器进行单向通信。例如,当朋友更新其状态、股票代码、新闻 Feed 或其他自动数据推送机制时。换句话说,客户端 Web SQL 数据库或 IndexedDB 对象存储的更新。如果您需要将数据发送到服务器,XMLHttpRequest
始终是您的合适之选。
SSE 通过 HTTP 发送。无需任何特殊的协议或服务器实现即可使用。WebSocket 需要全双工连接和新的 WebSocket 服务器来处理协议。
此外,服务器发送的事件还具有 WebSocket 在设计上所缺少的各种功能,包括自动重新连接、事件 ID 和发送任意事件的功能。
使用 JavaScript 创建 EventSource
如需订阅事件流,请创建一个 EventSource
对象并向其传递该数据流的网址:
const source = new EventSource('stream.php');
接下来,为 message
事件设置处理程序。您可以选择监听 open
和 error
:
source.addEventListener('message', (e) => {
console.log(e.data);
});
source.addEventListener('open', (e) => {
// Connection was opened.
});
source.addEventListener('error', (e) => {
if (e.readyState == EventSource.CLOSED) {
// Connection was closed.
}
});
从服务器推送更新时,会触发 onmessage
处理程序,并且新数据会显示在其 e.data
属性中。神奇之处在于,每当连接关闭时,浏览器都会在大约 3 秒后自动重新连接到来源。您的服务器实现甚至可以控制此重新连接超时。
大功告成。您的客户现在可以处理来自 stream.php
的事件。
事件流格式
从来源发送事件流只需构建纯文本响应,响应采用 text/event-stream
Content-Type,遵循 SSE 格式。基本形式的响应应包含一个 data:
行,后跟您的消息,后跟两个“\n”字符以结束数据流:
data: My message\n\n
多行数据
如果消息较长,可以使用多行 data:
将其拆分。以 data:
开头的两行或更多行将被视为单段数据,这意味着仅触发一个 message
事件。
每行都应以一个“\n”结尾(最后一行除外,它应以两个结尾)。传递给 message
处理程序的结果是由换行符串联的单个字符串。例如:
data: first line\n
data: second line\n\n</pre>
这会在 e.data
中生成“第一行\n 第二行”。然后,您可以使用 e.data.split('\n').join('')
重建不含“\n”字符的消息。
发送 JSON 数据
使用多行有助于您在不破坏语法的情况下发送 JSON:
data: {\n
data: "msg": "hello world",\n
data: "id": 12345\n
data: }\n\n
处理该数据流可能的客户端代码:
source.addEventListener('message', (e) => {
const data = JSON.parse(e.data);
console.log(data.id, data.msg);
});
将 ID 与事件相关联
您可以通过添加以 id:
开头的行,随流式事件发送唯一 ID:
id: 12345\n
data: GOOG\n
data: 556\n\n
通过设置 ID,浏览器可以跟踪上次触发的事件,以便在与服务器的连接中断时,通过新请求设置特殊的 HTTP 标头 (Last-Event-ID
)。这样,浏览器就可以确定适合触发哪个事件。message
事件包含 e.lastEventId
属性。
控制重新连接超时
浏览器会在每个连接关闭大约 3 秒后尝试重新连接到来源。您可以添加一个以 retry:
开头、后跟在尝试重新连接之前需等待的毫秒数的行,以此方式更改该超时。
以下示例会在 10 秒后尝试重新连接:
retry: 10000\n
data: hello world\n\n
指定事件名称
单个事件来源可以通过添加事件名称来生成不同类型的事件。如果存在以 event:
开头的行,后跟事件的唯一名称,则该事件会与该名称相关联。在客户端上,您可以设置一个事件监听器来监听这一特定事件。
例如,以下服务器输出发送了三种类型的事件,即通用的“message”事件、“userlogon”和“update”事件:
data: {"msg": "First message"}\n\n
event: userlogon\n
data: {"username": "John123"}\n\n
event: update\n
data: {"username": "John123", "emotion": "happy"}\n\n
在客户端设置事件监听器时:
source.addEventListener('message', (e) => {
const data = JSON.parse(e.data);
console.log(data.msg);
});
source.addEventListener('userlogon', (e) => {
const data = JSON.parse(e.data);
console.log(`User login: ${data.username}`);
});
source.addEventListener('update', (e) => {
const data = JSON.parse(e.data);
console.log(`${data.username} is now ${data.emotion}`);
};
服务器示例
以下是使用 PHP 实现的基本服务器实现:
<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache'); // recommended to prevent caching of event data.
/**
* Constructs the SSE data format and flushes that data to the client.
*
* @param string $id Timestamp/id of this connection.
* @param string $msg Line of text that should be transmitted.
**/
function sendMsg($id, $msg) {
echo "id: $id" . PHP_EOL;
echo "data: $msg" . PHP_EOL;
echo PHP_EOL;
ob_flush();
flush();
}
$serverTime = time();
sendMsg($serverTime, 'server time: ' . date("h:i:s", time()));
?>
以下是使用 Express 处理程序在 Node JS 上实现的类似实现:
app.get('/events', (req, res) => {
// Send the SSE header.
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
// Sends an event to the client where the data is the current date,
// then schedules the event to happen again after 5 seconds.
const sendEvent = () => {
const data = (new Date()).toLocaleTimeString();
res.write("data: " + data + '\n\n');
setTimeout(sendEvent, 5000);
};
// Send the initial event immediately.
sendEvent();
});
sse-node.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<script>
const source = new EventSource('/events');
source.onmessage = (e) => {
const content = document.createElement('div');
content.textContent = e.data;
document.body.append(content);
};
</script>
</body>
</html>
取消事件流
通常,浏览器会在连接关闭时自动重新连接到事件来源,但您可以从客户端或服务器取消该行为。
要从客户端取消流,请调用:
source.close();
如需从服务器取消数据流,请使用非 text/event-stream
Content-Type
进行响应,或返回 200 OK
以外的 HTTP 状态(例如 404 Not Found
)。
这两种方法都可以阻止浏览器重新建立连接。
安全性说明
EventSource 生成的请求与其他网络 API(如提取)遵循相同的源政策。如果您需要从不同来源访问服务器上的 SSE 端点,请参阅如何使用跨域资源共享 (CORS) 来启用该端点。