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

发布于 2023-05-13 23:01:21 字数 9301 浏览 82 评论 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技术交流群

发布评论

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

关于作者

三月梨花

暂无简介

0 文章
0 评论
560 人气
更多

推荐作者

已经忘了多久

文章 0 评论 0

15867725375

文章 0 评论 0

LonelySnow

文章 0 评论 0

走过海棠暮

文章 0 评论 0

轻许诺言

文章 0 评论 0

信馬由缰

文章 0 评论 0

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