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

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:" + "内容" + "nn"

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进行消息推送保证你看的清清楚楚

SSE实现消息推送

我们后端来使用node+express来实现一下SSE消息推送。
我们需要创建一个 express项目,然后安装express和cors。
然后我们创建 routes/sse/infoPush.js文件。
这个文件用来实现SSE消息推送。
1.我们需要告诉客户端消息类型
2.告诉浏览器不要直接使用缓存中的资源
3.使用setInterval不断发送消息
4.设置事件类型event和事件名称sseEvent
5.给每个事件分配一个唯一的标识符
6.客户端与服务器之间的连接意外关闭,等待多长时间尝试重新连接
7.构建SSE消息:"data: " + 消息 + "nn"
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:sseEventn) 需要和客户端保持一致。      * 它表示的是事件类型event和事件名称sseEvent      * sse.addEventListener("sseEvent", (event) => { })      * 也就是说:需要和前端的addEventListener事件监听名称一样      * */      res.write(`event:sseEventn`);     // id 字段是SSE消息的一个可选部分,它允许为每个事件分配一个唯一的标识符。     res.write(`id:${index}n`);     /**      * 我们向SSE响应中添加一个 retry 字段,      * retry 字段指定如果客户端与服务器之间的连接意外关闭,      * 客户端在尝试重新连接之前应该等待的时间(以毫秒为单位)      * 这里我们设置等待5s后重新连接      * */      res.write(`retry: 5000n`);     /**      * 构建SSE消息:"data: " + 消息 + "nn"      * 两个连续的换行符 nn,表示消息的结束      * */      res.write("data: " + JSON.stringify({ content: new Date() }) + "nn");     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> 

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

我们多次点击出问题

我们发现多次点击出现了问题。无法正常关闭。
为啥会出现这样的问题:因为多次点击创建了多个实例对象。
在关闭的时候关闭的是最后一个,前面的那些都没有正常关闭。
解决办法:
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> 

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

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

前后端约定一个字段表示已经推送结束。
当前端检测到后,就认为已经结束推送结束,然后关闭连接。

<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> 

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

尾声

今天情人节,各位小伙伴们有啥打算。
我准备去垃圾桶看看能不能见到宝贝
不说了,现在先规划路径,拜拜啦

发表评论

评论已关闭。

相关文章