sse实现消息推送

协议描述

客户端

使用 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,数据格式示例如下:

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

使用场景

服务端推送数据到前端

示例代码

用 GET 发送请求

前端

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

javascript 复制代码
const source = new EventSource(url);
source.onmessage = (e) => {
  const data = JSON.parse(e.data);
  const message = data.message;

  this.list.push(message);
};
服务端
用 express 实现
javascript 复制代码
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 实现
javascript 复制代码
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.

javascript 复制代码
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;
});
浏览器运行效果

用非 GET 的 method 发送请求

前端

安装 @microsoft/fetch-event-source

shell 复制代码
npm i -S @microsoft/fetch-event-source
javascript 复制代码
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

javascript 复制代码
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 请求会等到流结束后才返回,而不是分批返回

现象
原因

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

错误代码
javascript 复制代码
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;
});
解决
javascript 复制代码
router.get("/xxx", async (ctx) => {});

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

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

现象
原因

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

错误代码
javascript 复制代码
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

相关推荐
勤奋的小王同学~1 小时前
(功能测试Charles)如何抓取手机http的报文
网络·网络协议·http
chao_7891 小时前
HTTP 响应状态码
网络·网络协议·http
浠寒AI1 小时前
FastAPI核心解密:深入“路径操作”与HTTP方法,构建API的坚实骨架
网络协议·http·fastapi
程序猿小D2 小时前
第29节 Node.js Query Strings
node.js·vim·express
2501_915106325 小时前
无需 Mac,使用Appuploader简化iOS上架流程
websocket·网络协议·tcp/ip·http·网络安全·https·udp
爱分享的程序员7 小时前
前端面试专栏-基础篇:5. HTTP/2 协议深度解析
网络·网络协议·http
bing_1587 小时前
Spring Boot 项目中Http 请求如何对响应体进行压缩
spring boot·后端·http
ArabySide7 小时前
【JavaScript】 HTTP Cookie 核心知识梳理与常用的封装实现
javascript·计算机网络·http·web
武子康8 小时前
Java-43 深入浅出 Nginx - 基本配置方式 nginx.conf Events块 HTTP块 反向代理 负载均衡
java·后端·nginx·http·负载均衡·运维开发
Code_Geo8 小时前
基于 HTTP 的单向流式通信协议SSE详解
网络·网络协议·http