当您希望对 Web 应用程序进行实时更新时,您可以依靠老式的定期轮询或尝试使用一些具有推送功能的现代技术。您的第一个冲动可能是使用WebSockets。但是,如果您只想从服务器接收数据,您可以使用Server Sent Events。
传统上,网页必须向服务器发送请求才能接收新数据;即页面向服务器请求数据。使用server-sent events,服务器可以通过将消息推送到网页来随时向网页发送新数据。这些传入的消息可以被视为网页内的事件+数据。
您可以查看这篇文章以了解 SSE 和 Websocket 之间的区别,并对何时使用其中之一发表自己的看法。对于我的用例,定期从服务器接收更新,我将坚持使用 SSE。
使用 Koa 的 SSE 基础知识
让我们从构建一个基于 Koa 的 HTTP 服务器开始。
- 它将有一个以 200 状态响应的捕获所有路由。
- 它将有一个
/sse
端点,在收到请求时,它将通过调整一些套接字参数来确保我们的连接保持打开状态,并返回适当的 HTTP 标头以启动新的 SSE 流。 - 我们将创建一个新的PassThrough流(一个简单地将输入字节传递到输出的流)并将其作为我们的响应主体传递。
- 最后,我们将使用一个简单的间隔生成一个数据馈送,该间隔将定期将当前时间戳写入流;因此,通过打开的连接将该数据推送到客户端。
const Koa = require("koa");
const { PassThrough } = require("stream");
new Koa().
use(async (ctx, next) => {
if (ctx.path !== "/sse") {
return await next();
}
ctx.request.socket.setTimeout(0);
ctx.req.socket.setNoDelay(true);
ctx.req.socket.setKeepAlive(true);
ctx.set({
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
});
const stream = new PassThrough();
ctx.status = 200;
ctx.body = stream;
setInterval(() => {
stream.write(`data: ${new Date()}\n\n`);
}, 1000);
})
.use(ctx => {
ctx.status = 200;
ctx.body = "ok";
})
.listen(8080, () => console.log("Listening"));
注意两点:第一,输出数据必须符合SSE格式。其次,必须返回一个流作为主体响应,以确保 Koa 不会关闭连接。您可以深入 Koa 源代码(检查此方法)以查看 Koa 如何处理响应。如果您看一下,您会看到 Koa 会将正文内容发送到 HTTP 响应流,除非您使用另一个流作为正文。在这种情况下,它将通过管道传输流;因此,在我们关闭 PassThrough 流之前,不会关闭响应流。
要在浏览器中测试我们的 SSE 流,我们应该使用EventSource API。
http://localhost:8080
在浏览器中访问,打开控制台并删除以下代码段以使用服务器消息。
const source = new EventSource("http://localhost:8080/sse");
source.onopen = () => console.log("Connected");
source.onerror = console.error;
source.onmessage = console.log;
关闭流
如果您重新加载浏览器或关闭源(使用该close()
方法),您的服务器将中断。间隔将尝试在流上写入,然后……它消失了!
我们在处理闭包时必须小心,在这种情况下是我们的流。我们的时间间隔不知道它必须停止向流提供数据。
为了解决这个问题,我们可以将自己附加到流close
事件并从数据馈送中“取消订阅”。
const interval = setInterval(() => {
stream.write(`data: ${new Date()}\n\n`);
}, 1000);
stream.on("close", () => {
clearInterval(interval);
});
广播数据
前面的示例为每个连接的客户端生成一个新的数据馈送。在现实世界的场景中,我们还希望将相同的数据广播到不同的客户端。
我们可以添加一个简单的EventEmitter并将数据生成移到连接代码之外,看看它是如何工作的。
const Koa = require("koa");
const { PassThrough } = require("stream")
const EventEmitter = require("events");
const events = new EventEmitter();
events.setMaxListeners(0);
const interval = setInterval(() => {
events.emit("data", new Date() });
}, 1000);
new Koa().
use(async (ctx, next) => {
if (ctx.path !== "/sse") {
return await next();
}
ctx.request.socket.setTimeout(0);
ctx.req.socket.setNoDelay(true);
ctx.req.socket.setKeepAlive(true);
ctx.set({
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
});
const stream = new PassThrough();
ctx.status = 200;
ctx.body = stream;
const listener = (data) => {
stream.write(`data: ${data}\n\n`);
}
events.on("data", listener);
stream.on("close", () => {
events.off("data", listener);
});
})
.use(ctx => {
ctx.status = 200;
ctx.body = "ok";
})
.listen(8080, () => console.log("Listening"));
格式化流数据
如我之前提到的,SSE 流数据必须符合标准化格式。为了减轻从数据对象转换为 SSE 消息的 痛苦,我们将使用自定义流 Transformer交换我们的 PassThrough 流。
- 转换器(第 5 行)将对象转换为 SSE 文本格式。为了简化示例,我们将只处理纯数据消息(第 13 行)。
- 我们的数据馈送将发出一个带有
timestamp
键的数据对象(第 22 行)。 - 我们的事件侦听器会将原始数据写入转换器(第 46 行)。
const Koa = require("koa"); const { Transform } = require("stream"); const EventEmitter = require("events"); class SSEStream extends Transform { constructor() { super({ writableObjectMode: true, }); } _transform(data, _encoding, done) { this.push(`data: ${JSON.stringify(data)}\n\n`); done(); } } const events = new EventEmitter(); events.setMaxListeners(0); const interval = setInterval(() => { events.emit("data", { timestamp: new Date() }); }, 1000); new Koa(). use(async (ctx, next) => { if (ctx.path !== "/sse") { return await next(); } ctx.request.socket.setTimeout(0); ctx.req.socket.setNoDelay(true); ctx.req.socket.setKeepAlive(true); ctx.set({ "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", }); const stream = new SSEStream(); ctx.status = 200; ctx.body = stream; const listener = (data) => { stream.write(data); }; events.on("data", listener); stream.on("close", () => { events.off("data", listener); }); }) .use(ctx => { ctx.status = 200; ctx.body = "ok"; }) .listen(8080, () => console.log("Listening"));
我们还必须对我们的客户端进行微小的调整以处理新的 JSON 格式的数据。
加起来
这只是一个简单的例子来说明 SSE 技术:在 Node 中使用它是多么容易,以及如何使用流将 SSE 构建到 Koa 服务中。您可以扩展示例的 SSEStream 以支持所有事件流格式等等。
正如我在本文开头提到的,在处理服务器到客户端的单向通信时,您可以在 websocket 和服务 器发送事件之间进行选择。每一种都有优点和缺点。学习它们并选择适合您的问题的一种。
撰稿人