SSE进行消息推送保证你看的清清楚楚

SSE简介

SSE(Server-Sent Events)是一种实现服务器主动向客户端推送数据的技术,也称为 "事件流"。

它基于 HTTP 协议,是一个get请求。

利用了其长连接特性,从而实现:服务器向客户端的实时数据推送。

但客户端不能通过 SSE 向服务端发送数据。因此它是单向通信的。

SSE 的连接状态仅有三种:已连接、连接中、已断开。

连接状态是由浏览器自动维护的,客户端无法手动关闭或重新打开连接。

eventSource 的连接状态

readyState 属性表示当前 EventSource 对象的状态。

它是一个只读属性,它的值有以下几个:

CONNECTING:表示正在和服务器建立连接。此时:readyState的值是0

OPEN:表示已经建立连接,正在接收服务器发送的数据。

此时:readyState的值是1

CLOSED:表示连接已经被关闭,无法再接收服务器发送的数据。

此时:readyState的值是2

SSE 和 WebSocket 的区别

1.通信方式不同: SSE是单向通信的。WebSocket是双向通信的。

2.协议不同: SSE基于HTTP协议,是一个get请求。WebSocket 一般基于TCP协议。

3.跨域问题:SSE是不能够跨域的(HTTP协议,get请求)。 WebSocket 是可以跨域的。

4.重连机制:SSE浏览器会自动重连。WebSocket需要手动实现重连机制

5.传输数据不同: SSE只能够传输纯文本,不支持直接发送二进制数据。WebSocket支持发送文本和二进制数据。

服务端基本响应格式

event:自定义事件类型。客户端可以根据不同的事件类型来执行不同的操作。

id:事件的唯一标识符。客户端可以使用这个ID来恢复事件流。

retry:建议的重新连接时间(毫秒)。如果连接中断,客户端将等待这段时间后尝试重新连接。

data:事件的数据。如果数据跨越多行,每行都应该以data:开始。"data:" + "内容" + "\n\n"

res.setHeader("Connection", "keep-alive");

在HTTP/1.1协议中,Connection头用于控制网络连接的持久性。

即是否保持TCP连接打开以后,便于后续的请求和响应可以通过同一个连接发送。

而不是每个请求都建立一个新的连接。

这样助于减少建立和关闭TCP连接所需的时间和资源,从而提高性能。

在HTTP/1.1中,默认情况下连接就是持久性的(keep-alive),除非特别指定为close。

但一些旧的HTTP/1.0客户端或代理中,可能需要显式设置Connection: keep-alive头来请求持久连接。

对于现代的Web应用来说,通常不需要手动设置这个头,因为大多数客户端和服务器都默认支持持久连接。

小提醒:如果你不确认http版本,那就加上。

否则会出现没有保持持久连接的情况下,每次隔一次请求就要重新连接一次,图表/表格/页面刷新会不流畅。

res.setHeader("Cache-Control", "no-cache");

控制客户端(如浏览器)和中间代理服务器对响应的缓存行为。

允许缓存,但强制验证。

客户端或代理服务器可以缓存响应,但在每次使用缓存前,必须进行校验。

如果服务器确认缓存有效,则使用缓存;否则返回新数据。

或者说:防止使用过期缓存,确保客户端不会直接使用本地缓存,而是始终与服务器确认数据的最新性。

它的适用场景:

动态数据:如实时更新的内容(股票价格、新闻推送)。

个性化内容:如用户特定的数据(购物车、个人资料)。

SSE:确保客户端不会缓存事件流数据。

ngix配置问题

SSE实现消息推送

我们后端来使用node+express来实现一下SSE消息推送。

我们需要创建一个 express项目,然后安装express和cors。

然后我们创建 routes/sse/infoPush.js文件。

这个文件用来实现SSE消息推送。

1.我们需要告诉客户端消息类型

2.告诉浏览器不要直接使用缓存中的资源

3.使用setInterval不断发送消息

4.设置事件类型event和事件名称sseEvent

5.给每个事件分配一个唯一的标识符

6.客户端与服务器之间的连接意外关闭,等待多长时间尝试重新连接

7.构建SSE消息:"data: " + 消息 + "\n\n"

8.当客户端点击关闭时,我们清除定时器,并且结束推送

// app.js
const express=require("express");
const path=require("path")
// 处理跨域的插件
const cors = require('cors')
// SSE相关信息路由 
const sseInfoRouter = require('./routes/sse/infoPush'); 
const app= express();
// 使用跨域插件
app.use(cors())
// 当以/public/ 开头的时候,去./public/ 目录中去找对应的资源
app.use(express.static(path.join(__dirname, '/public')));
app.use('/sse', sseInfoRouter);

//端口
app.listen(3000,function () {
  console.log("127.0.0.1:3000")
});

// routes/sse/infoPush.js 文件
const express = require("express");
const router = express.Router();
router.get("/ai/question/push", (req, res) => {
  // 设置 SSE 响应类型(告诉客户端响应类型,这是一个SSE事件流)
  res.setHeader("Content-Type", "text/event-stream;charset=utf-8");
  /**
   * 告诉浏览器不要直接使用缓存中的资源,而是应该向服务器发送请求来检查该资源是否有更新。
   * 确保用户获取到最新内容是非常有用,尤其是在内容频繁更新的Web应用中。
   * */ 
  res.setHeader("Cache-Control", "no-cache");
  // 用于控制网络连接的持久性。
  res.setHeader("Connection", "keep-alive");
  // 告诉浏览器,来自任何源的请求都可以被接受并访问该资源。可以跨域
  res.setHeader("Access-Control-Allow-Origin", "*");
  let index = 0;
  const timer = setInterval(() => {
    /**
     * 下面的res.write(event:sseEvent\n) 需要和客户端保持一致。
     * 它表示的是事件类型event和事件名称sseEvent
     * sse.addEventListener("sseEvent", (event) => { })
     * 也就是说:需要和前端的addEventListener事件监听名称一样
     * */ 
    res.write(`event:sseEvent\n`);
    // id 字段是SSE消息的一个可选部分,它允许为每个事件分配一个唯一的标识符。
    res.write(`id:${index}\n`);
    /**
     * 我们向SSE响应中添加一个 retry 字段,
     * retry 字段指定如果客户端与服务器之间的连接意外关闭,
     * 客户端在尝试重新连接之前应该等待的时间(以毫秒为单位)
     * 这里我们设置等待5s后重新连接
     * */ 
    res.write(`retry: 5000\n`);
    /**
     * 构建SSE消息:"data: " + 消息 + "\n\n"
     * 两个连续的换行符 \n\n,表示消息的结束
     * */ 
    res.write("data: " + JSON.stringify({ content: new Date() }) + "\n\n");
    index++;
    console.log(index)
  }, 1000);

  // 当客户端点击关闭时,我们清除定时器,并且结束推送
  req.on("close", () => {
    clearInterval(timer);
    res.end();
  });
});

module.exports = router;

EventSource() 构造函数的介绍

EventSource 对象是 HTML5 新增的一个客户端 API。

用于服务器实时推送数据到客户端,它是单向的。

const eventSource = new EventSource(url, options);

参数url:必填,建立起与服务器的连接,并开始接收服务器发送的数据

参数options:Object 类型,表示可选参数。

withCredentials:Boolean 类型,表示是否允许发送 Cookie 和 HTTP 认证信息。默认为 false。

下面这2个参数都是没有的,我看见有些博客写了,但是我在mdn上,并没有看见。

headers:Object 类型,表示要发送的请求头信息。 [没有这个参数]

retryInterval:Number 类型,表示与服务器失去连接后,重新连接的时间间隔。默认为 1000 毫秒。[没有这个参数]

使用EventSource接收数据并渲染

<template>
  <div class="chat-box">
    <button @click="startConnectHandler">建立连接</button>
    <button @click="endConnectHandler">关闭连接</button>
    <h2>
      连接状态{{ stateData }}
    </h2>
    <h2>下面就是返回来的数据</h2>
    <div>
      <div v-for="(item, index) in list" :key="index">
        {{ item }}
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      eventSource: null,
      stateData: null,
      list: [],
      connectStatus:false,
    };
  },
  created() {},
  methods: {
    startConnectHandler() {
      let url = "http://127.0.0.1:3000/sse/ai/question/push?title=请你介绍一下SSE?";
      // 表示与服务器建立连接的 URL。必填。
      const sseObj  = new EventSource(url);
      this.eventSource = sseObj;
      console.log('状态',sseObj,this.eventSource)
      
      if (sseObj.readyState === 0) {
        //sseObj.readyState === EventSource.CONNECTING 也可以判断正在连接服务器
        console.log('0:"正在连接服务器...');
      } 
      
      sseObj.onopen = (e) => {
        if(sseObj.readyState === 1){
          // sseObj.readyState === EventSource.OPEN 也可以判断连接成功
          let data = `SSE 连接成功,状态${ sseObj.readyState}, 对象${e}`;
          this.stateData = data;
          console.log("1:SSE 连接成功");
        }
      };
      // 接收消息,这个事件需要和后端保持一致哈
      // 后端的事件名称:sseEvent
      sseObj.addEventListener("sseEvent", (event) => {
        const data = JSON.parse(event.data);
        this.list.push(data.content);
        console.log("这次消息推送的内容event:", event);
      });
      sseObj.onerror = (e) => {
        console.log("error", e);
      };
    },
    endConnectHandler() {
      if(this.eventSource){
        this.eventSource.close();
        if(this.eventSource.readyState === 2) {
          // sseObj.readyState === EventSource.CLOSED 也可以判断连接已经关闭
          console.log('2连接已经关闭。',this.eventSource, this.eventSource.readyState);
        }
        console.log("end");
      }
    },
  },
};
</script>
<style scoped>
.chat-box{
  padding-left: 20px;
  padding-top: 20px;
  button{
    margin-right: 20px;
    padding: 6px;
  }
}
</style>

我们多次点击出问题

我们发现多次点击出现了问题。无法正常关闭。

为啥会出现这样的问题:因为多次点击创建了多个实例对象。

在关闭的时候关闭的是最后一个,前面的那些都没有正常关闭。

解决办法:

1.创建连接后给创建按钮禁用。

2.使用单例模式

避免多次重复连接:创建连接后给按钮禁用

<template>
  <div class="chat-box">
    <button @click="startConnectHandler" :disabled="connectStatus" >建立连接</button>
    <button @click="endConnectHandler">关闭连接</button>
    <h2>
      <p>连接状态{{ this.eventSource && this.eventSource.readyState }}</p> 
    </h2>
    <h2>下面就是返回来的数据</h2>
    <div>
      <div v-for="(item, index) in list" :key="index">
        {{ item }}
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      eventSource: null,
      stateData: null,
      list: [],
      connectStatus:false,
    };
  },
  created() {},
  methods: {
    startConnectHandler() {
      let url = "http://127.0.0.1:3000/sse/ai/question/push?title=请你介绍一下SSE?";
      
      // 表示与服务器建立连接的 URL。必填。
      const sseObj  = new EventSource(url);
      this.eventSource = sseObj;
      console.log('状态',sseObj,this.eventSource)
      
      if (sseObj.readyState === 0) {
        this.connectStatus = true
        //sseObj.readyState === EventSource.CONNECTING 也可以判断正在连接服务器
        console.log('0:"正在连接服务器...');
      } 
      
      sseObj.onopen = (e) => {
        if(sseObj.readyState === 1){
          // sseObj.readyState === EventSource.OPEN 也可以判断连接成功
          let data = `SSE 连接成功,状态${ sseObj.readyState}, 对象${e}`;
          this.stateData = data;
          console.log("1:SSE 连接成功");
        }
      };
      // 接收消息,这个事件需要和后端保持一致哈
      // 后端的事件名称:sseEvent
      sseObj.addEventListener("sseEvent", (event) => {
        const data = JSON.parse(event.data);
        this.list.push(data.content);
        console.log("这次消息推送的内容event:", event);
      });
      sseObj.onerror = (e) => {
        console.log("error", e);
      };
    },
    endConnectHandler() {
      if(this.eventSource){
        this.connectStatus = false
        this.eventSource.close();
        if(this.eventSource.readyState === 2) {
          // sseObj.readyState === EventSource.CLOSED 也可以判断连接已经关闭
          console.log('2连接已经关闭。',this.eventSource, this.eventSource.readyState);
        }
        console.log("end");
      }
    },
  },
};
</script>
<style scoped>
.chat-box{
  padding-left: 20px;
  padding-top: 20px;
  button{
    margin-right: 20px;
    padding: 6px;
  }
}
</style>

推送完消息如何断开[完整版]

前后端约定一个字段表示已经推送结束。

当前端检测到后,就认为已经结束推送结束,然后关闭连接。

<template>
  <div class="chat-box">
    <button @click="startConnectHandler" :disabled="connectStatus" >建立连接</button>
    <button @click="endConnectHandler">关闭连接</button>
    <h2>
      <p>连接状态{{ this.eventSource && this.eventSource.readyState }}</p> 
    </h2>
    <h2>下面就是返回来的数据</h2>
    <div>
      <div v-for="(item, index) in list" :key="index">
        {{ item }}
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      eventSource: null,
      stateData: null,
      list: [],
      connectStatus:false,
    };
  },
  created() {},
  methods: {
    startConnectHandler() {
      let url = "http://127.0.0.1:3000/sse/ai/question/push?title=请你介绍一下SSE?";
      
      // 表示与服务器建立连接的 URL。必填。
      const sseObj  = new EventSource(url);
      this.eventSource = sseObj;
      console.log('状态',sseObj,this.eventSource)
      
      if (sseObj.readyState === 0) {
        this.connectStatus = true
        //sseObj.readyState === EventSource.CONNECTING 也可以判断正在连接服务器
        console.log('0:"正在连接服务器...');
      } 
      
      sseObj.onopen = (e) => {
        if(sseObj.readyState === 1){
          // sseObj.readyState === EventSource.OPEN 也可以判断连接成功
          let data = `SSE 连接成功,状态${ sseObj.readyState}, 对象${e}`;
          this.stateData = data;
          console.log("1:SSE 连接成功");
        }
      };
      // 接收消息,这个事件需要和后端保持一致哈
      // 后端的事件名称:sseEvent
      sseObj.addEventListener("sseEvent", (event) => {
        const data = JSON.parse(event.data);
        //如果最后推送的是 'contDnd',说明推送已经完了。此时关闭连接
        if(data.content==='contDnd'){
          this.endConnectHandler()
        }else{
          this.list.push(data.content);
        }
        console.log("这次消息推送的内容event:", data.content);
      });
      sseObj.onerror = (e) => {
        console.log("error", e);
      };
    },
    endConnectHandler() {
      if(this.eventSource){
        this.connectStatus = false
        this.eventSource.close();
        if(this.eventSource.readyState === 2) {
          // sseObj.readyState === EventSource.CLOSED 也可以判断连接已经关闭
          console.log('2连接已经关闭。',this.eventSource, this.eventSource.readyState);
        }
        console.log("end");
      }
    },
  },
};
</script>
<style scoped>
.chat-box{
  padding-left: 20px;
  padding-top: 20px;
  button{
    margin-right: 20px;
    padding: 6px;
  }
}
</style>

尾声

今天情人节,各位小伙伴们有啥打算。

我准备去垃圾桶看看能不能见到宝贝

不说了,现在先规划路径,拜拜啦