piIfuC 发表于 2025-2-14 17:56:58

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.jsconst 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>
尾声

今天情人节,各位小伙伴们有啥打算。
我准备去垃圾桶看看能不能见到宝贝
不说了,现在先规划路径,拜拜啦
页: [1]
查看完整版本: SSE进行消息推送保证你看的清清楚楚