如何通过SSE实现后端向前端实时推送消息?

在前端开发里,实现服务器和客户端高效通信很重要,SSE(Server-Sent Events)技术就能让服务器主动给客户端推送数据。这篇文章会详细讲讲SSE,像它的原理、和WebSocket的差异,还有用node + express搭建消息推送的具体步骤,新手也能轻松上手!

一、SSE技术是什么?

SSE,也就是服务器发送事件,是专门用来实现服务器主动向客户端推送数据的技术,也被叫做“事件流” 。它是基于HTTP协议的,发起的是一个get请求。利用HTTP的长连接特性,服务器就能把实时数据推送给客户端。不过它有个特点,客户端没办法反过来给服务器发数据,属于单向通信。

SSE连接状态总共就三种:已连接、连接中、已断开 。这些状态是由浏览器自动管理的,我们没办法手动去关闭或者重新打开连接。在JavaScript里,EventSource对象的readyState属性可以获取当前连接状态,是个只读属性,取值0代表正在和服务器建立连接(CONNECTING),1表示已经建立连接,能接收数据(OPEN),2则是连接关闭,不能再接收数据了(CLOSED)。

二、SSE和WebSocket有啥不同?

  1. 通信方式:SSE是单向的,只有服务器能给客户端发数据;WebSocket则是双向通信,客户端和服务器能互相发消息。
  2. 协议基础:SSE依托HTTP协议,走的是get请求;WebSocket一般基于TCP协议。
  3. 跨域能力:因为基于HTTP的get请求,SSE不支持跨域;WebSocket可以跨域,使用起来更灵活。
  4. 重连机制:要是连接断了,SSE的浏览器会自动尝试重连;WebSocket就得手动编写重连代码来实现这个功能。
  5. 传输数据类型:SSE只能传输纯文本;WebSocket不仅能传文本,还支持二进制数据。

三、服务端响应格式有讲究

在使用SSE时,服务端的响应格式很重要:

  • event:这个是自定义的事件类型,客户端可以根据不同的类型执行不同操作,就像给消息贴上分类标签一样。
  • id:每个事件的唯一标识符,客户端能利用它恢复事件流,方便追踪和管理消息。
  • retry:当连接中断,它用来设置客户端重新连接前等待的时间,单位是毫秒。
  • data:存放事件的数据内容。如果数据有多行,每一行都要以“data:”开头,格式是“data:内容\n\n”,两个连续换行表示一条数据结束。

另外,在设置HTTP响应头时,“Connection: keep-alive”是为了保持TCP连接打开,这样后续的请求和响应就能通过同一个连接发送,减少建立和关闭连接的开销,提升性能。在HTTP/1.1协议里,默认是开启持久连接的,但在一些旧的HTTP/1.0客户端或代理中,可能需要手动设置。“Cache-Control: no-cache”是控制缓存行为的,它允许缓存,但每次使用前都要校验,防止客户端使用过期缓存,保证获取到最新数据,对于SSE这种实时数据推送场景很关键。

四、nginx配置要点

如果在实际项目中使用SSE,涉及到,有几个地方要注意。比如设置服务器监听端口、域名,还有处理反向代理时,要正确配置proxy_pass、proxy_set_header等参数,像下面这样:

server {
    listen 80;
    server_name openai.zuol1.com;
    location / {
        proxy_pass http://127.0.0.1:9000; 
        # 修复sse eventSource接收不到消息的问题
        proxy_set_header Connection proxy_http_version 1.1; 
        chunked_transfer_encoding off;
        proxy_buffering off;
        proxy_cache off;
    }
}

这些配置主要是确保SSE消息能正常在服务器和客户端之间传递,避免出现接收不到消息等问题。

五、用node + express实现SSE消息推送

接下来,我们用node + express搭建一个SSE消息推送的示例。

  1. 项目初始化和依赖安装:先创建一个express项目,然后安装express和cors这两个依赖包。express是Web应用框架,cors用来解决跨域问题。
  2. 编写消息推送代码:在项目里创建routes/sse/infoPush.js文件,专门实现SSE消息推送功能。
// 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");
    // 告诉浏览器不要直接使用缓存中的资源,每次都要向服务器检查资源是否更新
    res.setHeader("Cache-Control", "no-cache");
    // 保持网络连接的持久性
    res.setHeader("Connection", "keep-alive");
    // 允许来自任何源的请求访问该资源,解决跨域问题
    res.setHeader("Access-Control-Allow-Origin", "*");

    let index = 0;
    // 每隔1秒发送一次消息
    const timer = setInterval(() => {
        // 设置事件类型为sseEvent,要和前端监听的事件名称一致
        res.write(`event:sseEvent\n`);
        // 给每个事件分配一个唯一标识符
        res.write(`id:${index}\n`);
        // 设置连接意外关闭后,客户端等待5秒再尝试重新连接
        res.write(`retry: 5000\n`);
        // 构建SSE消息,这里发送当前时间作为消息内容
        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;

3. 整合到主应用:在app.js里引入这个路由,并配置静态资源路径、跨域等。

// 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')));
// 挂载SSE消息推送的路由
app.use('/sse', sseInfoRouter);

// 启动服务器,监听3000端口
app.listen(3000, function () {
    console.log("127.0.0.1:3000");
});

六、前端接收SSE数据

前端可以用HTML5新增的EventSource API来接收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() {
            // 与服务器建立连接的URL
            let url = "http://127.0.0.1:3000/sse/ai/question/push?title=请你介绍一下SSE?";
            const sseObj = new EventSource(url);
            this.eventSource = sseObj;
            console.log('状态', sseObj, this.eventSource);

            if (sseObj.readyState === 0) {
                this.connectStatus = true;
                console.log('0:"正在连接服务器...');
            } 

            sseObj.onopen = (e) => {
                if (sseObj.readyState === 1) {
                    let data = `SSE 连接成功,状态${sseObj.readyState}, 对象${e}`;
                    this.stateData = data;
                    console.log("1:SSE 连接成功");
                }
            };
            // 监听后端发送的sseEvent事件,接收消息
            sseObj.addEventListener("sseEvent", (event) => {
                const data = JSON.parse(event.data);
                // 如果接收到特定结束标识,关闭连接
                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) {
                    console.log('2连接已经关闭。', this.eventSource, this.eventSource.readyState);
                }
                console.log("end");
            }
        }
    }
};
</script>

<style scoped>
.chat-box {
    padding-left: 20px;
    padding-top: 20px;
}
.chat-box button {
    margin-right: 20px;
    padding: 6px;
}
</style>

这里有个小问题,多次点击建立连接按钮会创建多个实例对象,关闭时只关闭了最后一个。解决办法可以是建立连接后禁用按钮,或者使用单例模式。上面代码里采用了禁用按钮的方式,通过connectStatus变量来控制按钮状态。

七、总结与拓展

SSE技术在实时数据推送场景,像股票行情展示、新闻推送等方面有很大优势。过这篇文章,大家对SSE的原理、使用方法应该有了比较清晰的认识。如果在实际项目中需要更复杂的功能,比如更精准的消息控制、结合其他技术优化性能等,可以进一步探索和实践。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

给TA打赏
共{{data.count}}人
人已打赏
技术教程

Siri通过快捷指令集成DeepSeek API图文操作流程

2025-2-14 15:27:23

技术教程

使用腾讯元宝接入Deepseek-R1满血版实现联网搜索教程

2025-2-15 10:13:30

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
今日签到
有新私信 私信列表
搜索