使用服务端事件流更新数据

发布于 2023-05-13 23:01:21 字数 9301 浏览 85 评论 0

到底什么是事件流?,我不会感到惊讶。 如果您偶然看到这篇文章并想知道“服务器发送事件 ( SSE ) 许多人从未听说过它们,这是理所当然的。 多年来,该规范发生了重大变化,API 在某种程度上已经让位于更新、更性感的通信协议,例如 WebSocket API 。 背后的想法 SSE 可能很熟悉:Web 应用程序订阅服务器生成的更新流,每当发生新事件时,都会向客户端发送通知。 但要真正理解 Server-Sent Events,我们需要了解其 AJAX 前辈的局限性,其中包括:

轮询 是绝大多数 AJAX 应用程序使用的传统技术。 基本思想是应用程序反复轮询服务器以获取数据。 如果您熟悉 HTTP 协议,就会知道获取数据是围绕请求/响应格式进行的。 客户端发出请求并等待服务器响应数据。 如果没有可用的,则返回空响应。 那么投票有什么大不了的呢? 额外的轮询会产生更大的 HTTP 开销。

长轮询(Hanging GET / COMET) 是轮询的一种细微变化。 在长轮询中,如果服务器没有可用数据,服务器会保持请求打开,直到有新数据可用。 因此,这种技术通常被称为挂起 GET。 当信息可用时,服务器响应,关闭连接,并重复该过程。 效果是服务器不断响应可用的新数据。 缺点是此类过程的实施通常涉及黑客攻击,例如将脚本标签附加到无限 iframe。 我们可以比黑客做得更好!

另一方面,Server-Sent Events 是从头开始设计的,以提高效率。 使用 SSE 进行通信时,服务器可以随时将数据推送到您的应用程序,而无需发出初始请求。 换句话说,更新可以在发生时从服务器流式传输到客户端。 SSE 在服务器和客户端之间打开一个单向通道。

Server-Sent Events 和长轮询的主要区别在于 SSE 由浏览器直接处理,用户只需监听消息即可。

服务器发送的事件与 WebSockets

为什么选择服务器发送事件而不是 WebSockets? 好问题。

一个原因 SSE 一直隐藏在阴影中的 是因为后来的 API(如WebSockets) 提供了更丰富的协议来执行双向、全双工通信。 拥有双向通道对于游戏、消息传递应用程序以及需要双向近乎实时更新的情况更具吸引力。 但是,在某些情况下, 不需要从客户端发送数据 。 您只需要来自某些服务器操作的更新。 一些例子是朋友的状态更新、股票代码、新闻提要或其他自动数据推送机制(例如更新客户端 Web SQL 数据库或 IndexedDB 对象存储)。 如果您需要将数据发送到服务器, XMLHttpRequest永远是朋友。

SSE 通过传统的 HTTP 发送。 这意味着它们 不需要特殊的协议或服务器实现 即可工作。 另一方面,WebSockets 需要全双工连接和新的 Web Socket 服务器来处理协议。 此外,Server-Sent Events 具有 WebSockets 在设计上缺乏的各种功能,例如 自动重新连接 、 事件 ID 能力 和发送任意事件的 。

JavaScript API

要订阅事件流,请创建一个 EventSource对象并将流的 URL 传递给它:

if (!!window.EventSource) {
  var source = new EventSource('stream.php');
} else {
  // Result to xhr polling :(
}

注意: 如果 URL 传递给 EventSourceconstructor 是一个绝对 URL,它的来源(方案、域、端口)必须与调用页面的来源匹配。

接下来,为 message事件。 您可以选择收听 openerror:

source.addEventListener('message', function(e) {
  console.log(e.data);
}, false);

source.addEventListener('open', function(e) {
  // Connection was opened.
}, false);

source.addEventListener('error', function(e) {
  if (e.readyState == EventSource.CLOSED) {
    // Connection was closed.
  }
}, false);

当从服务器推送更新时, onmessage处理程序触发并且新数据在其 e.data财产。 神奇的是,每当连接关闭时,浏览器都会在 ~3 秒后自动重新连接到源。 您的服务器实现甚至可以控制此重新连接超时。 请参阅 控制重新连接超时。 下一节中的

就是这样。 您的客户现在已准备好处理来自 stream.php.

事件流格式

从源发送事件流是构建明文响应的问题,由 text/event-streamContent-Type,遵循 SSE 格式。 在其基本形式中,响应应包含“ data:" 行,然后是您的消息,然后是两个“\n”字符以结束流:

data: My message\n\n

多行数据

如果您的消息较长,您可以使用多个“ data:" 行。以 " 开头的两个或多个连续行 data:" 将被视为单个数据,即只有一个 message事件将被解雇。 每行应以单个“\n”结尾(最后一行除外,它应以两个结尾)。 结果传递给你 messagehandler 是由换行符连接的单个字符串。 例如:

data: first line\n
data: second line\n\n

将产生“第一行\n第二行” e.data. 然后可以使用 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', function(e) {
  var data = JSON.parse(e.data);
  console.log(data.id, data.msg);
}, false);

将 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:" 后跟事件的唯一名称,事件与该名称相关联。在客户端上,可以设置事件侦听器来侦听该特定事件。

例如,以下服务器输出发送三种类型的事件,通用的“消息”事件、“用户登录”和“更新”事件:

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', function(e) {
  var data = JSON.parse(e.data);
  console.log(data.msg);
}, false);

source.addEventListener('userlogon', function(e) {
  var data = JSON.parse(e.data);
  console.log('User login:' + data.username);
}, false);

source.addEventListener('update', function(e) {
  var data = JSON.parse(e.data);
  console.log(data.username + ' is now ' + data.emotion);
}, false);

服务器示例

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()));

下载代码

类似实现 这是一个使用Node JS 的

var http = require('http');
var sys = require('sys');
var fs = require('fs');

http.createServer(function(req, res) {
  //debugHeaders(req);

  if (req.headers.accept && req.headers.accept == 'text/event-stream') {
    if (req.url == '/events') {
      sendSSE(req, res);
    } else {
      res.writeHead(404);
      res.end();
    }
  } else {
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.write(fs.readFileSync(__dirname + '/sse-node.html'));
    res.end();
  }
}).listen(8000);

function sendSSE(req, res) {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive'
  });

  var id = (new Date()).toLocaleTimeString();

  // Sends a SSE every 5 seconds on a single connection.
  setInterval(function() {
    constructSSE(res, id, (new Date()).toLocaleTimeString());
  }, 5000);

  constructSSE(res, id, (new Date()).toLocaleTimeString());
}

function constructSSE(res, id, data) {
  res.write('id: ' + id + '\n');
  res.write("data: " + data + '\n\n');
}

function debugHeaders(req) {
  sys.puts('URL: ' + req.url);
  for (var key in req.headers) {
    sys.puts(key + ': ' + req.headers[key]);
  }
  sys.puts('\n\n');
}

下载代码

sse-node.html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
</head>
<body>
  <script>
    var source = new EventSource('/events');
    source.onmessage = function(e) {
      document.body.innerHTML += e.data + '<br>';
    };
  </script>
</body>
</html>

取消事件流

通常,浏览器会在连接关闭时自动重新连接到事件源,但可以从客户端或服务器取消该行为。

要从客户端取消流,只需调用:

source.close();

要从服务器取消流,用非“ text/event-streamContent-Type或者返回一个 HTTP 状态 200 OK(例如 404 Not Found).

这两种方法都会阻止浏览器重新建立连接。

关于安全的一句话

来自 WHATWG 关于 跨文档消息传递安全的 部分:

作者应该检查 origin 属性以确保只接受来自他们期望从中接收消息的域的消息。 否则,作者的消息处理代码中的错误可能会被恶意站点利用。

因此,作为额外的预防措施,请务必验证 e.origin在你的 message处理程序与您的应用程序的来源相匹配:

source.addEventListener('message', function(e) {
  if (e.origin != 'http://example.com') {
    alert('Origin was not http://example.com');
    return;
  }
  ...
}, false);

另一个好主意是检查您收到的数据的完整性:

此外,即使在检查了 origin 属性之后,作者也应该检查有问题的数据是否具有预期的格式......

演示

我用 PHP 编写了一个 演示应用程序 ,它使时钟与服务器保持同步。

参考

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

三月梨花

暂无简介

文章
评论
562 人气
更多

推荐作者

微信用户

文章 0 评论 0

小情绪

文章 0 评论 0

ゞ记忆︶ㄣ

文章 0 评论 0

笨死的猪

文章 0 评论 0

彭明超

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文