使用服务器发送的事件流式传输更新

服务器发送的事件 (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 事件设置处理程序。您可以选择监听 openerror

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) 来启用该端点。