使用服务端事件流更新数据
到底什么是事件流?,我不会感到惊讶。 如果您偶然看到这篇文章并想知道“服务器发送事件 ( 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 传递给 EventSource
constructor 是一个绝对 URL,它的来源(方案、域、端口)必须与调用页面的来源匹配。
接下来,为 message
事件。 您可以选择收听 open
和 error
:
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-stream
Content-Type,遵循 SSE 格式。 在其基本形式中,响应应包含“ data:
" 行,然后是您的消息,然后是两个“\n”字符以结束流:
data: My message\n\n
多行数据
如果您的消息较长,您可以使用多个“ data:
" 行。以 " 开头的两个或多个连续行 data:
" 将被视为单个数据,即只有一个 message
事件将被解雇。 每行应以单个“\n”结尾(最后一行除外,它应以两个结尾)。 结果传递给你 message
handler 是由换行符连接的单个字符串。 例如:
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-stream
“ Content-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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论