Skip to content

SSE

什么是 SSE

​ SSE(Server Sent Event) 既是服务端发送消息,是一种轻量级长连接协议,和 websocket 不同,socket 可以双端接受发送消息,而 SSE 只能由服务端发送消息,并且 SSE 自动会重连.

从“服务端主动向浏览器实时推送消息”这一点来看,该 API 与 WebSockets API 有一些相似之处。但是,该 API 与 WebSockers API 的不同之处在于:

Server-Sent Events APIWebSockets API
基于 HTTP 协议基于 TCP 协议
单工,只能服务端单向发送消息全双工,可以同时发送和接收消息
轻量级,使用简单相对复杂
内置断线重连和消息追踪的功能不在协议范围内,需手动实现
文本或使用 Base64 编码和 gzip 压缩的二进制消息类型广泛
支持自定义事件类型不支持自定义事件类型
连接数 HTTP/1.1 6 个,HTTP/2 可协商(默认 100)连接数无限制

​ 服务器发送事件(SSE)受到打开连接数的限制,这个限制是对于浏览器的,并且设置为非常低的数字(6),打开多个选项卡时可能会特别痛苦。

​ SSE 应用场景:ChatGPT.

ChatGPT 这种需要耗费大量时间计算才能得到最终结果,不适用与 Ajax,因为 Ajax 只能一次请求响应一次结果,也就是说要想得到完整数据,就必须等待 GPT 把全部的计算完成才能返回,中间可能会耗费很多时间.

SSE 浏览器上使用

使用浏览器提供的EventSource来完成 SSE 通信,简单易用.通过addEventListener来实现自定义事件的监听,回调可以接受一个参数,这个参数就是服务端响应的数据.

EventSource内置了三种事件:

open :连接成功就触发

error :出错或关闭时触发

message:服务端响应的 SSE 报文数据中 event 字段为message或无 evetn 字段就触发该事件回调。

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta
      http-equiv="X-UA-Compatible"
      content="IE=edge" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>你好</h1>
    <button>开启连接</button>
    <button>断开连接</button>
    <div class="list"></div>
    <script>
      const [start, end] = document.getElementsByTagName("button");
      const list = end.nextElementSibling;

      let eventSource = null;

      const startSSE = () => {
        eventSource = new EventSource("/stream");
        eventSource.addEventListener("customEvent", (event) => {
          console.log(event);
          const li = document.createElement("li");
          li.innerText = JSON.stringify(event.data);
          list.append(li);
        });
        eventSource.onopen = () => {
          console.log("SSE连接成功!");
        };
        eventSource.onerror = () => {
          console.log("SSE连接失败!");
        };
      };

      const closeSSE = () => {
        if (eventSource) {
          eventSource.close();
          console.log("关闭连接!");
        }
      };

      start.onclick = startSSE;
      end.onclick = closeSSE;
    </script>
  </body>
</html>
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta
      http-equiv="X-UA-Compatible"
      content="IE=edge" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>你好</h1>
    <button>开启连接</button>
    <button>断开连接</button>
    <div class="list"></div>
    <script>
      const [start, end] = document.getElementsByTagName("button");
      const list = end.nextElementSibling;

      let eventSource = null;

      const startSSE = () => {
        eventSource = new EventSource("/stream");
        eventSource.addEventListener("customEvent", (event) => {
          console.log(event);
          const li = document.createElement("li");
          li.innerText = JSON.stringify(event.data);
          list.append(li);
        });
        eventSource.onopen = () => {
          console.log("SSE连接成功!");
        };
        eventSource.onerror = () => {
          console.log("SSE连接失败!");
        };
      };

      const closeSSE = () => {
        if (eventSource) {
          eventSource.close();
          console.log("关闭连接!");
        }
      };

      start.onclick = startSSE;
      end.onclick = closeSSE;
    </script>
  </body>
</html>

SSE 服务端上使用

SSE 的响应头部的设置

​ 首先 SSE 协议的报文必须要设置的响应头部为content-type cache-control connection

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

SSE API 规定推送事件流的 MIME 类型为 text/event-stream

必须指定浏览器不缓存服务端发送的数据,以确保浏览器可以实时显示服务端发送的数据。

SSE 是一个一直保持开启的 TCP 连接,所以 Connection 为 keep-alive。

SSE 报文消息格式

EventStream(事件流)为 UTF-8 格式编码的文本或使用 Base64 编码和 gzip 压缩的二进制消息。

​ 每条消息由一行或多行字段(eventidretrydata)组成,每个字段组成形式为:字段名:字段值。字段以行为单位,每行一个(即以 \n 结尾)。以冒号开头的行为注释行,会被浏览器忽略。

​ 每次推送,可由多个消息组成,每个消息之间以空行分隔(即最后一个字段以\n\n结尾)。

📢 注意:

  • 除上述四个字段外,其他所有字段都会被忽略。
  • 如果一行字段中不包含冒号,则整行文本将被视为字段名,字段值为空。
  • 注释行可以用来防止链接超时,服务端可以定期向浏览器发送一条消息注释行,以保持连接不断。

1. event

​ 事件类型。如果指定了该字段,则在浏览器收到该条消息时,会在当前 EventSource 对象(见 4)上触发一个事件,事件类型就是该字段的字段值。可以使用 addEventListener 方法在当前 EventSource 对象上监听任意类型的命名事件。

​ 如果该条消息没有 event 字段,则会触发 EventSource 对象 onmessage 属性上的事件处理函数。如果接收消息中有一个 event 字段,触发的事件与 event 字段的值相同。如果不存在 event 字段,则将触发通用的 message 事件

2. id

​ 事件 ID。事件的唯一标识符,浏览器会跟踪事件 ID,如果发生断连,浏览器会把收到的最后一个事件 ID 放到 HTTP Header Last-Event-Id 中进行重连,作为一种简单的同步机制。

​ 例如可以在服务端将每次发送的事件 ID 值自动加 1,当浏览器接收到该事件 ID 后,下次与服务端建立连接后再请求的 Header 中将同时提交该事件 ID,服务端检查该事件 ID 是否为上次发送的事件 ID,如果与上次发送的事件 ID 不一致则说明浏览器存在与服务器连接失败的情况,本次需要同时发送前几次浏览器未接收到的数据。

3. retry

​ 重连时间。整数值,单位 ms,如果与服务器的连接丢失,浏览器将等待指定时间,然后尝试重新连接。如果该字段不是整数值,会被忽略。

​ 当服务端没有指定浏览器的重连时间时,由浏览器自行决定每隔多久与服务端建立一次连接(一般为 30s)。

4. data

​ 消息数据。数据内容只能以一个字符串的文本形式进行发送,如果需要发送一个对象时,需要将该对象以一个 JSON 格式的字符串的形式进行发送。在浏览器接收到该字符串后,再把它还原为一个 JSON 对象。

SSE 报文示例

​ 如下事件流示例,共发送了 4 条消息,每条消息间以一个空行\n\n作为分隔符。每条消息中的字段以\n分割。

​ 第一条仅仅是个注释,因为它以冒号开头。

​ 第二条消息只包含一个 data 字段,值为 'this is second message'。

​ 第三条消息包含两个 data 字段,其会被解析为一个字段,值为 'this is third message part 1\nthis is third message part 2'。

​ 第四条消息包含完整四个字段,指定了事件类型为 'server-time',事件 id 为 '1',重连时间为 '30000'ms,消息数据为 JSON 格式的 '{"text": "this is fourth message", "time": "12:00:00"}'。

js
// 注释
: this is first message\n\n
// 消息1
data: this is second message\n\n
// 消息2
data: this is third message part one\n
data this is third message part two\n\n
// 消息3
event: server-time\n
id: 1\n
retry: 30000\n
data: {"text": "this is fourth message", "time": "2023-04-09 12:00:00"}\n\n
// 注释
: this is first message\n\n
// 消息1
data: this is second message\n\n
// 消息2
data: this is third message part one\n
data this is third message part two\n\n
// 消息3
event: server-time\n
id: 1\n
retry: 30000\n
data: {"text": "this is fourth message", "time": "2023-04-09 12:00:00"}\n\n

示例代码如下

js
function getStream(req, res) {
  res.setHeader("Content-type", "text/event-stream"); // 开启sse协议
  res.setHeader("Cache-Control", "no-cache"); // 避免浏览器缓存
  res.setHeader("Connection", "keep-alive"); // sse是一个一直保持开启的 TCP 连接,所以 Connection 为 keep-alive
  let id = 1;
  // 当请求断开时,结束响应
  req.on("close", () => {
    id = 0;
    clearInterval(timer);
    res.end();
  });
  const timer = setInterval(() => {
    // 自定义事件名称customEvent
    res.write(`event: customEvent\n`);
    res.write(`id: ${id}\n`);
    res.write(`retry: 30000\n`);
    res.write(`data: ${JSON.stringify({ content: "你好!" })}\n\n`);
    id++;
    // 响应10次内容就结束本次HTTP传输
    if (id >= 10) {
      clearInterval(timer);
      res.end();
    }
  }, 1000);
}
function getStream(req, res) {
  res.setHeader("Content-type", "text/event-stream"); // 开启sse协议
  res.setHeader("Cache-Control", "no-cache"); // 避免浏览器缓存
  res.setHeader("Connection", "keep-alive"); // sse是一个一直保持开启的 TCP 连接,所以 Connection 为 keep-alive
  let id = 1;
  // 当请求断开时,结束响应
  req.on("close", () => {
    id = 0;
    clearInterval(timer);
    res.end();
  });
  const timer = setInterval(() => {
    // 自定义事件名称customEvent
    res.write(`event: customEvent\n`);
    res.write(`id: ${id}\n`);
    res.write(`retry: 30000\n`);
    res.write(`data: ${JSON.stringify({ content: "你好!" })}\n\n`);
    id++;
    // 响应10次内容就结束本次HTTP传输
    if (id >= 10) {
      clearInterval(timer);
      res.end();
    }
  }, 1000);
}

SSE 推送房间

基本步骤

  1. 服务端维护房间 id 的表,用来发送对应消息。
  2. 客户端发送 HTTP 请求,服务端在表中添加一个房间号 id,并返回给客户端
  3. 客户端创建 SSE 请求,开启长连接
  4. 客户端的EventSource需要监听房间号事件,服务端可以在需要推送的时候响应该事件的消息,这样客户端就能接收到数据了。
  5. 客户端发送 HTTP 请求,携带上房间号 id,服务器通过 id 删除表中的房间号。成功后客户端断开连接。

参考

  1. https://juejin.cn/post/7229632570374783034#heading-14