2020年11月30日

服务器发送事件

服务器发送事件 规范描述了一个内置类 EventSource,它保持与服务器的连接并允许接收来自服务器的事件。

类似于 WebSocket,连接是持久性的。

但是,它们之间存在一些重要的区别。

WebSocket EventSource
双向:客户端和服务器都可以交换消息 单向:只有服务器发送数据
二进制和文本数据 仅文本
WebSocket 协议 常规 HTTP

EventSource 是一个比 WebSocket 能力更弱的与服务器通信方式。

为什么有人会使用它呢?

主要原因是:它更简单。在许多应用程序中,WebSocket 的强大功能有点过剩。

我们需要从服务器接收数据流:可能是聊天消息或市场价格,或其他任何东西。这就是 EventSource 的长处。它还支持自动重新连接,而使用 WebSocket 时,我们需要手动实现此功能。此外,它只是一个普通的 HTTP,而不是一个新的协议。

获取消息

要开始接收消息,我们只需要创建 new EventSource(url)

浏览器将连接到 url 并保持连接打开,等待事件。

服务器应该以状态码 200 和 Content-Type: text/event-stream 头部响应,然后保持连接并将消息以特殊格式写入连接中,例如

data: Message 1

data: Message 2

data: Message 3
data: of two lines
  • 消息文本位于 data: 之后,冒号后的空格是可选的。
  • 消息以双行换行符 \n\n 分隔。
  • 要发送一个换行符 \n,我们可以立即再发送一个 data:(上面的第 3 条消息)。

在实践中,复杂的邮件通常以 JSON 编码发送。换行符在其中编码为 \n,因此不需要多行 data: 消息。

例如

data: {"user":"John","message":"First line\n Second line"}

…所以我们可以假设一个 data: 恰好包含一条消息。

对于每条这样的消息,都会生成 message 事件

let eventSource = new EventSource("/events/subscribe");

eventSource.onmessage = function(event) {
  console.log("New message", event.data);
  // will log 3 times for the data stream above
};

// or eventSource.addEventListener('message', ...)

跨域请求

EventSource 支持跨域请求,例如 fetch 和任何其他网络方法。我们可以使用任何 URL

let source = new EventSource("https://another-site.com/events");

远程服务器将获取 Origin 头部,并且必须以 Access-Control-Allow-Origin 响应才能继续。

要传递凭据,我们应该设置额外的选项 withCredentials,例如

let source = new EventSource("https://another-site.com/events", {
  withCredentials: true
});

有关跨域头的更多详细信息,请参见 Fetch: 跨域请求 章。

重新连接

创建后,new EventSource 将连接到服务器,如果连接断开,则重新连接。

这非常方便,因为我们不必关心它。

在重新连接之间存在一个很小的延迟,默认情况下为几秒钟。

服务器可以使用响应中的 retry:(以毫秒为单位)设置建议的延迟。

retry: 15000
data: Hello, I set the reconnection delay to 15 seconds

retry: 可以与一些数据一起出现,也可以作为独立消息出现。

浏览器应该等待这些毫秒后重新连接。或者更长,例如,如果浏览器知道(从操作系统)目前没有网络连接,它可能会等到连接出现,然后重试。

  • 如果服务器希望浏览器停止重新连接,它应该以 HTTP 状态 204 响应。
  • 如果浏览器想要关闭连接,它应该调用 eventSource.close()
let eventSource = new EventSource(...);

eventSource.close();

此外,如果响应具有不正确的 Content-Type 或其 HTTP 状态与 301、307、200 和 204 不同,则不会重新连接。在这种情况下,将发出 "error" 事件,浏览器不会重新连接。

请注意

当连接最终关闭时,无法“重新打开”它。如果我们想再次连接,只需创建一个新的 EventSource

消息 ID

当连接由于网络问题而断开时,任何一方都无法确定哪些消息已接收,哪些消息未接收。

为了正确地恢复连接,每条消息都应该有一个 id 字段,如下所示

data: Message 1
id: 1

data: Message 2
id: 2

data: Message 3
data: of two lines
id: 3

当收到带有 id: 的消息时,浏览器

  • 将属性 eventSource.lastEventId 设置为其值。
  • 在重新连接时,使用该 id 发送标头 Last-Event-ID,以便服务器可以重新发送后续消息。
id: 放在 data: 之后

请注意:id 由服务器附加在消息 data 下面,以确保在收到消息后更新 lastEventId

连接状态:readyState

EventSource 对象具有 readyState 属性,该属性具有三个值之一

EventSource.CONNECTING = 0; // connecting or reconnecting
EventSource.OPEN = 1;       // connected
EventSource.CLOSED = 2;     // connection closed

当创建对象或连接断开时,它始终为 EventSource.CONNECTING(等于 0)。

我们可以查询此属性以了解 EventSource 的状态。

事件类型

默认情况下,EventSource 对象会生成三个事件

  • message – 收到的消息,可作为 event.data 使用。
  • open – 连接已打开。
  • error – 无法建立连接,例如服务器返回 HTTP 500 状态。

服务器可以使用事件开始处的 event: ... 指定另一种类型的事件。

例如

event: join
data: Bob

data: Hello

event: leave
data: Bob

要处理自定义事件,我们必须使用 addEventListener,而不是 onmessage

eventSource.addEventListener('join', event => {
  alert(`Joined ${event.data}`);
});

eventSource.addEventListener('message', event => {
  alert(`Said: ${event.data}`);
});

eventSource.addEventListener('leave', event => {
  alert(`Left ${event.data}`);
});

完整示例

这是发送带有 123 的消息,然后发送 bye 并断开连接的服务器。

然后浏览器会自动重新连接。

结果
server.js
index.html
let http = require('http');
let url = require('url');
let querystring = require('querystring');
let static = require('node-static');
let fileServer = new static.Server('.');

function onDigits(req, res) {
  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() { // when "Start" button pressed
  if (!window.EventSource) {
    // IE or an old browser
    alert("The browser doesn't support EventSource.");
    return;
  }

  eventSource = new EventSource('digits');

  eventSource.onopen = function(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.

摘要

EventSource 对象自动建立持久连接,并允许服务器通过它发送消息。

它提供

  • 自动重连,可调整 retry 超时时间。
  • 消息 ID 用于恢复事件,最后接收到的标识符在重新连接时会发送到 Last-Event-ID 标头中。
  • 当前状态位于 readyState 属性中。

这使得 EventSource 成为 WebSocket 的可行替代方案,因为后者更底层,缺乏此类内置功能(尽管可以实现)。

在许多现实应用中,EventSource 的功能已经足够了。

所有现代浏览器(IE 除外)都支持。

语法是

let source = new EventSource(url, [credentials]);

第二个参数只有一个可能的选项:{ withCredentials: true },它允许发送跨域凭据。

总体跨域安全性与 fetch 和其他网络方法相同。

EventSource 对象的属性

readyState
当前连接状态:EventSource.CONNECTING (=0)EventSource.OPEN (=1)EventSource.CLOSED (=2)
lastEventId
最后接收到的 id。在重新连接时,浏览器会将其发送到 Last-Event-ID 标头中。

方法

close()
关闭连接。

事件

message
收到消息,数据位于 event.data 中。
open
连接已建立。
error
发生错误时,包括连接丢失(会自动重新连接)和致命错误。我们可以检查 readyState 来查看是否正在尝试重新连接。

服务器可以在 event: 中设置自定义事件名称。此类事件应使用 addEventListener 处理,而不是 on<event>

服务器响应格式

服务器发送消息,以 \n\n 分隔。

消息可能包含以下字段

  • data: – 消息正文,多个 data 序列被解释为单个消息,各部分之间用 \n 分隔。
  • id: – 重新生成 lastEventId,在重新连接时发送到 Last-Event-ID 中。
  • retry: – 建议以毫秒为单位的重新连接重试延迟。无法从 JavaScript 设置它。
  • event: – 事件名称,必须位于 data: 之前。

消息可以包含一个或多个字段,顺序任意,但 id: 通常位于最后。

教程地图

评论

在评论之前请阅读...
  • 如果您有改进建议,请提交 GitHub 问题或拉取请求,而不是评论。
  • 如果您无法理解文章中的某些内容,请详细说明。
  • 要插入少量代码,请使用<code>标签,对于多行代码,请使用<pre>标签,对于超过 10 行的代码,请使用沙盒(plnkrjsbincodepen…)