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]