
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>

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