sse实现消息推送

318 阅读1分钟

协议描述

客户端

使用 EventSource

服务端

添加响应头:Content-Type: text/event-stream

SSE 协议原生只支持 GET 请求,如果想使用非 GET 的请求来实现 SSE,需要安装三方库(下文有示例)。

时序

用顺序图表示整个交互过程,如下图所示:

message 数据格式

参考:www.ruanyifeng.com/blog/2017/0…

每一行都以 id: 或者 event: 或者 data: 或者 retry: 开头,以\n 结尾,最后一行以\n\n 结尾 [id/event/data/retry]: value\n 如果 data 有多行,格式如下: data: xxx\n data: yyy\n\n

如果返回的是复杂对象,需要对整个对象序列化。

如果服务端是 nodejs,数据格式示例如下:

`data: ${JSON.stringify({
  message: "Update from server",
})}\n`;

使用场景

服务端推送数据到前端

示例代码

用 GET 发送请求

前端

创建 EventSource 对象,设置 onmessage 的回调(可以用 addEventListener 注册回调)

const source = new EventSource(url);
source.onmessage = (e) => {
  const data = JSON.parse(e.data);
  const message = data.message;

  this.list.push(message);
};
服务端
用 express 实现
app.get("/rest/express/sse", async (req, res) => {
  res.set({
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  });
  res.flushHeaders();
  setInterval(() => {
    const data = {
      message: `Current time is ${new Date().toLocaleTimeString()}`,
    };
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  }, 1000);
});
用 koa 实现
基于 koa-sse-stream 实现
const sse = require("koa-sse-stream");
app.use(
  sse({
    maxClients: 5000,
    pingInterval: 30000,
  })
);
router.get("/rest/sse", async (ctx, next) => {
  ctx.header = {
    "Content-Type": "text/event-stream",
  };
  ctx.sse.send("get notice");
  for (let i = 0; i < 5; i++) {
    await new Promise((resolve) => {
      setTimeout(() => {
        ctx.sse.send("interval notice");
        resolve();
      }, 500);
    });
  }
  ctx.sse.sendEnd();
});
不使用 koa-sse-stream 实现

采用 nodejs 原生的 stream 实现,下面的示例采用了PassThrough stream.

const { PassThrough } = require("stream");

router.get("/rest/sse/native", (ctx, next) => {
  ctx.set({
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  });
  const stream = new PassThrough();
  ctx.status = 200;

  for (let i = 0; i < 5; i++) {
    setTimeout(() => {
      stream.write(`id: ${i}\n`);
      stream.write(
        `data: ${JSON.stringify({
          message: "Update from server",
        })}\n`
      );
      stream.write("retry: 1000\n");
      stream.write("\n\n");
    }, i * 1000);
  }
  ctx.body = stream;
});
浏览器运行效果

20240204_225504_image.png

20240204_225418_image.png

用非 GET 的 method 发送请求

前端

安装 @microsoft/fetch-event-source

npm i -S @microsoft/fetch-event-source
import { fetchEventSource } from "@microsoft/fetch-event-source";

fetchEventSource("/rest/sse", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    foo: "bar",
  }),
  onmessage: (ev) => {
    console.log(ev.data);
    this.list.push(ev.data);
  },
  onerror(err) {
    console.log("err: ", err);
  },
  signal: AbortController.signal,
});
服务端

使用 koa 时,需要安装 koa-sse-stream

router.post("/rest/sse", async (ctx, next) => {
  ctx.header = {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache, no-transform",
  };
  ctx.sse.send("post notice");

  for (let i = 0; i < 5; i++) {
    await new Promise((resolve) => {
      setTimeout(() => {
        ctx.sse.send("interval post notice");
        resolve();
      }, 200);
    });
  }
  ctx.sse.sendEnd();
});

遇到的问题

sse 请求会等到流结束后才返回,而不是分批返回

现象

20240204_232306_image.png

原因

回调函数中,streamawait 阻塞,没有立即返回

错误代码
router.get("/rest/sse/native", async (ctx, next) => {
  ctx.set({
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  });
  const stream = new PassThrough();
  ctx.status = 200;

  for (let i = 0; i < 5; i++) {
    await new Promise((resolve) => {
      // 这行前面不能加await
      setTimeout(() => {
        stream.write(
          `data: ${JSON.stringify({
            message: "Update from server",
          })}\n`
        );
        stream.write("retry: 1000\n");
        stream.write("\n\n");
        resolve();
      }, i * 1000);
    });
  }
  ctx.body = stream;
});
解决
router.get("/xxx", async (ctx) => {});

回调如果有将 stream.write 封装成 promise,不要 await 这个 promise.

使用 nodejs 的 PassThrough stream,返回的 message 中,data 为空

现象

20240204_233059_image.png

原因

没有使用 koa-sse-stream 来返回 message,但是 koa 应用又引入了koa-sse-stream

错误代码
const Koa = require("koa");
const sse = require("koa-sse-stream");
const app = new Koa();
// app应用不应该引入koa-sse-stream
app.use(
  sse({
    maxClients: 5000,
    pingInterval: 30000,
  })
);
router.get("/rest/sse/native", (ctx, next) => {
  ctx.set({
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  });
  const stream = new PassThrough();
  ctx.status = 200;

  for (let i = 0; i < 5; i++) {
    setTimeout(() => {
      stream.write(
        `data: ${JSON.stringify({
          message: "Update from server",
        })}\n`
      );
      stream.write("retry: 1000\n");
      stream.write("\n\n");
    }, i * 1000);
  }
  ctx.body = stream;
});
解决

koa应用取消引入 koa-sse-stream