服务器发送的事件 (SSE) 通过 HTTP 连接从服务器向客户端发送自动更新。建立连接后,服务器可以发起数据传输。
您可能想要使用 SSE 从 Web 应用发送推送通知。SSE 是单向发送信息,因此您不会收到来自客户端的更新。
SSE 的概念可能已经不陌生。Web 应用会“订阅”服务器生成的更新流,每当发生新事件时,都会向客户端发送通知。但要真正了解服务器发送的事件,我们需要了解其 AJAX 前身的限制。其中包括:
轮询:应用会反复轮询服务器以获取数据。大多数 AJAX 应用都使用此技术。使用 HTTP 协议时,提取数据围绕请求和响应格式展开。客户端发出请求,并等待服务器响应数据。如果没有可用的数据,则返回空响应。额外的轮询会产生更大的 HTTP 开销。
长轮询(挂起 GET / COMET):如果服务器没有可用数据,则会将请求保持为打开状态,直到有新数据可用。因此,此技术通常称为“挂起 GET”。当信息可用时,服务器会做出响应并关闭连接,然后重复此过程。因此,服务器会不断使用新数据做出响应。如需进行此设置,开发者通常会使用一些技巧,例如将脚本标记附加到“无限”iframe。
服务器发送的事件从一开始就以高效为目标进行设计。与 SSE 通信时,服务器可以随时向您的应用推送数据,而无需发出初始请求。换句话说,更新可以随时从服务器流式传输到客户端。SSE 会在服务器和客户端之间打开单个单向通道。
服务器发送的事件与长轮询之间的主要区别在于,SSE 由浏览器直接处理,而用户只需监听消息即可。
服务器发送的事件与 WebSocket
为什么要选择服务器发送的事件而不是 WebSocket?这个问题问得好。
WebSocket 协议丰富,支持双向全双工通信。对于游戏、即时通讯应用以及需要双向近乎实时更新的任何用例,双向通道更为合适。
不过,有时您只需要服务器进行单向通信。例如,当好友更新状态、股票行情、新闻动态或其他自动数据推送机制时。换句话说,是对客户端 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
的事件。
事件流格式
从来源发送事件流的过程是构建一个纯文本响应,并使用遵循 SSE 格式的 text/event-stream
Content-Type 进行传送。在基本形式下,响应应包含 data:
行,后跟您的消息,再后跟两个“\n”字符以结束数据流:
data: My message\n\n
多行数据
如果消息较长,可以使用多行 data:
将其拆分。以 data:
开头的连续两行或更多行将被视为一条数据,这意味着系统只会触发一次 message
事件。
每行都应以一个“\n”结尾(最后一行除外,它应以两个“\n”结尾)。传递给 message
处理程序的结果是一个由换行符串联的字符串。例如:
data: first line\n
data: second line\n\n</pre>
这会在 e.data
中生成“first line\nsecond line”。然后,您可以使用 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(例如 fetch)相同的同源政策的约束。如果您需要从其他来源访问服务器上的 SSE 端点,请参阅如何使用跨源资源共享 (CORS) 进行启用。