SSE—实时信息推送

SSE(Server-Sent Events,服务器发送事件)是一种基于 HTTP 的单向通信协议,允许服务器主动、持续地向客户端推送数据,而无需客户端反复发起请求

核心特点

  1. 单项通信:仅服务器向客户端实时推送符合条件的信息;
  2. 基于HTTP:无需额外协议,兼容性好;
  3. 自动重连:原生支持断开后自动建立连接

如何实现SSE

核心原理

  • 服务器端:设置响应头为content0Type:text/event-stream,保持HTTP连接不关闭,持续输出符合SSE格式的文本;
  • 客户端:使用浏览器原生的EventSource对象连接服务器,监听消息事件

服务端实现

  • 技术栈:Express + node.js
js 复制代码
const express = require('express')
const app = express();
const port = 3000;

// 允许跨域(方便前端页面访问)
app.use((req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
    next();
});

// 静态文件托管(如果前端页面和后端同目录,可通过 http://localhost:3000/index.html 访问)
app.use(express.static(__dirname));

//设置SSE响应头
app.get('/api/sse',(req,res)=>{
    res.status(200);
    res.set({
        'Content-Type': 'text/event-stream', // 核心:声明事件流格式
        'Cache-Control': 'no-cache', // 禁止缓存
        'Connection': 'keep-alive', // 保持长连接
    })
    console.log('客户端建立了 SSE 连接');
    //每个一秒发送一次数据
    const interval = setInterval(() => {
        try {
            // 1. 推送普通消息(默认 event: message)
            // Express 5.x 要求 res.write() 入参为 Buffer 类型,需手动转换
            const randomData = {
                time: new Date().toLocaleTimeString(),
                value: Math.floor(Math.random() * 100), // 0-99 随机数
                type: 'normal'
            };
            const normalMessage = `data: ${JSON.stringify(randomData)}\n\n`;
            res.write(Buffer.from(normalMessage)); // 关键适配:转为 Buffer

            // 2. 推送自定义事件(比如 "alert" 事件)
            if (randomData.value > 80) { // 模拟阈值告警
                const alertData = {
                    time: new Date().toLocaleTimeString(),
                    msg: `数值超过阈值!当前值:${randomData.value}`,
                    level: 'warning'
                };
                const alertMessage = `event: alert\ndata: ${JSON.stringify(alertData)}\n\n`;
                res.write(Buffer.from(alertMessage)); // 关键适配:转为 Buffer
            }
        } catch (err) {
            console.error('推送数据失败:', err);
            clearInterval(interval);
            res.end();
        }
    }, 1000);

    // 客户端断开连接时清理定时器,释放资源
    req.on('close', () => {
        console.log('客户端断开 SSE 连接');
        clearInterval(interval);
        res.end();
    });
})


// 启动服务器
app.listen(port, () => {
    console.log(`SSE 演示服务器(Express 5.x)已启动:http://localhost:${port}`);
    console.log('请打开 http://localhost:3000/index.html 查看效果');
});

代码详解

  1. 基础依赖和服务器初始化
js 复制代码
const express = require('express')
const app = express();
const port = 3000;
  • 引入express 然后初始化一个express实例
  • 定义服务器监听的端口号
js 复制代码
app.use((req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
    next();
});
  • 设置允许跨域,允许不同源进行请求,方便后续操作
js 复制代码
app.use(express.static(__dirname));
  • 这是一个静态文件托管中间件

SSE 核心接口

js 复制代码
app.get('/api/sse',(req,res)=>{
    res.status(200);
    res.set({
        'Content-Type': 'text/event-stream', // 核心:声明事件流格式
        'Cache-Control': 'no-cache', // 禁止缓存
        'Connection': 'keep-alive', // 保持长连接
    })
    console.log('客户端建立了 SSE 连接');
    //每个一秒发送一次数据
    const interval = setInterval(() => {
        try {
            // 1. 推送普通消息(默认 event: message)
            // Express 5.x 要求 res.write() 入参为 Buffer 类型,需手动转换
            const randomData = {
                time: new Date().toLocaleTimeString(),
                value: Math.floor(Math.random() * 100), // 0-99 随机数
                type: 'normal'
            };
            const normalMessage = `data: ${JSON.stringify(randomData)}\n\n`;
            res.write(Buffer.from(normalMessage)); // 关键适配:转为 Buffer

            // 2. 推送自定义事件(比如 "alert" 事件)
            if (randomData.value > 80) { // 模拟阈值告警
                const alertData = {
                    time: new Date().toLocaleTimeString(),
                    msg: `数值超过阈值!当前值:${randomData.value}`,
                    level: 'warning'
                };
                const alertMessage = `event: alert\ndata: ${JSON.stringify(alertData)}\n\n`;
                res.write(Buffer.from(alertMessage)); // 关键适配:转为 Buffer
            }
        } catch (err) {
            console.error('推送数据失败:', err);
            clearInterval(interval);
            res.end();
        }
    }, 1000);

    // 客户端断开连接时清理定时器,释放资源
    req.on('close', () => {
        console.log('客户端断开 SSE 连接');
        clearInterval(interval);
        res.end();
    });
})
  • res.status(200):首先设置HTTP响应状态码

  • res.set(...):批量设置响应头,这 3 个是 SSE 的核心头:

  • text/event-stream:告诉客户端这是 SSE 事件流,不是普通 JSON/HTML;

  • no-cache:禁用缓存,确保客户端每次收到的都是实时数据;

  • keep-alive:让 HTTP 连接不关闭,服务器能持续推送数据

  • interval:设置定时器,进行定时推送

    • setInterval(..., 1000):创建定时器,每 1000ms(1 秒)执行一次推送逻辑;
  • try/catch:捕获推送过程中的异常(如客户端断开连接导致 res.write() 失败);

  • 普通消息格式data: ${JSON.stringify(randomData)}\n\n

    • data: 是 SSE 固定前缀,后面跟具体数据;
    • JSON.stringify():将对象转为字符串(SSE 只能传输文本);
    • \n\n:必须结尾,是 SSE 消息的结束标识;
  • Express 5.x 适配Buffer.from(normalMessage)

    • Express 5.x 对 res.write() 的入参类型做了严格限制,必须是 Buffer / 字符串(部分版本仅支持 Buffer),这里手动转 Buffer 确保兼容;
  • 自定义事件(alert)

    • 格式:event: alert\ndata: ...\n\nevent: alert 声明事件名为 alert
    • 客户端需通过 eventSource.addEventListener('alert', ...) 监听,而非默认的 onmessage
    • 作用:区分不同类型的消息(如普通数据、告警、通知),方便前端分类处理。
js 复制代码
req.on('close', () => {
    console.log('客户端断开 SSE 连接');
    clearInterval(interval);
    res.end();
});
  • req.on('close', ...):监听客户端断开连接事件(如前端关闭页面、主动关闭 EventSource);

  • clearInterval(interval):销毁定时器,否则定时器会一直运行,导致服务器内存泄漏;

  • res.end():主动关闭 HTTP 连接,释放服务器资源。

javascript 复制代码
app.listen(port, () => {
    console.log(`SSE 演示服务器(Express 5.x)已启动:http://localhost:${port}`);
    console.log('请打开 http://localhost:3000/index.html 查看效果');
});
  • app.listen(port, ...):启动服务器,监听 3000 端口;

  • 回调函数:服务器启动成功后打印提示信息,方便开发者知道访问地址

客户端实现

xml 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>SSE 实时推送演示(Express 5.x)</title>
    <style>
        body { padding: 20px; font-family: Arial; }
        .normal { color: #333; margin: 5px 0; }
        .alert { color: #e63946; background: #fef0f0; padding: 8px; margin: 5px 0; border-radius: 4px; }
        #status { color: #2a9d8f; font-weight: bold; margin-bottom: 10px; }
    </style>
</head>
<body>
<h1>SSE 实时数据推送演示(Express 5.x)</h1>
<div id="status">连接状态:未连接</div>
<h3>普通数据推送</h3>
<div id="normal-data"></div>
<h3>告警数据推送</h3>
<div id="alert-data"></div>

<script>
    // 获取页面元素
    const statusEl = document.getElementById('status');
    const normalDataEl = document.getElementById('normal-data');
    const alertDataEl = document.getElementById('alert-data');

    // 检查浏览器是否支持 SSE
    if (!!window.EventSource) {
        // 创建 SSE 连接
        const es = new EventSource('http://localhost:3000/api/sse');

        // 连接成功(open 事件)
        es.onopen = () => {
            statusEl.textContent = '连接状态:已建立 SSE 连接';
            statusEl.style.color = '#2a9d8f';
        };

        // 接收普通消息(message 事件)
        es.onmessage = (e) => {
            const data = JSON.parse(e.data);
            const item = document.createElement('div');
            item.className = 'normal';
            item.textContent = `[${data.time}] 随机数值:${data.value}`;
            normalDataEl.prepend(item);
            // 只保留最新 10 条
            if (normalDataEl.children.length > 10) {
                normalDataEl.removeChild(normalDataEl.lastChild);
            }
        };

        // 接收自定义 alert 事件
        es.addEventListener('alert', (e) => {
            const data = JSON.parse(e.data);
            const item = document.createElement('div');
            item.className = 'alert';
            item.textContent = `[${data.time}] ${data.msg}`;
            alertDataEl.prepend(item);
            // 只保留最新 5 条告警
            if (alertDataEl.children.length > 5) {
                alertDataEl.removeChild(alertDataEl.lastChild);
            }
        });

        // 连接错误(error 事件)
        es.onerror = (e) => {
            if (es.readyState === EventSource.CLOSED) {
                statusEl.textContent = '连接状态:连接已关闭(将自动重试)';
                statusEl.style.color = '#e9c46a';
            } else {
                statusEl.textContent = '连接状态:出错 - ' + (e.message || '未知错误');
                statusEl.style.color = '#e63946';
            }
        };

        // 页面关闭时断开连接
        window.onbeforeunload = () => {
            es.close();
        };
    } else {
        statusEl.textContent = '连接状态:你的浏览器不支持 SSE!';
        statusEl.style.color = '#e63946';
    }
</script>
</body>
</html>

在这里我不对布局进行讲解,我们来看主要的SSE接收部分

js 复制代码
if (!!window.EventSource) {
    // 创建 SSE 连接
    const es = new EventSource('http://localhost:3000/api/sse');

    // 连接成功(open 事件)
    es.onopen = () => {
        statusEl.textContent = '连接状态:已建立 SSE 连接';
        statusEl.style.color = '#2a9d8f';
    };

    // 接收普通消息(message 事件)
    es.onmessage = (e) => {
        const data = JSON.parse(e.data);
        const item = document.createElement('div');
        item.className = 'normal';
        item.textContent = `[${data.time}] 随机数值:${data.value}`;
        normalDataEl.prepend(item);
        // 只保留最新 10 条
        if (normalDataEl.children.length > 10) {
            normalDataEl.removeChild(normalDataEl.lastChild);
        }
    };

    // 接收自定义 alert 事件
    es.addEventListener('alert', (e) => {
        const data = JSON.parse(e.data);
        const item = document.createElement('div');
        item.className = 'alert';
        item.textContent = `[${data.time}] ${data.msg}`;
        alertDataEl.prepend(item);
        // 只保留最新 5 条告警
        if (alertDataEl.children.length > 5) {
            alertDataEl.removeChild(alertDataEl.lastChild);
        }
    });

    // 连接错误(error 事件)
    es.onerror = (e) => {
        if (es.readyState === EventSource.CLOSED) {
            statusEl.textContent = '连接状态:连接已关闭(将自动重试)';
            statusEl.style.color = '#e9c46a';
        } else {
            statusEl.textContent = '连接状态:出错 - ' + (e.message || '未知错误');
            statusEl.style.color = '#e63946';
        }
    };

    // 页面关闭时断开连接
    window.onbeforeunload = () => {
        es.close();
    };
} else {
    statusEl.textContent = '连接状态:你的浏览器不支持 SSE!';
    statusEl.style.color = '#e63946';
}
  • 首先对浏览器的兼容性进行检测,检测是否兼容EventSource,如果不兼容,就执行如下内容
js 复制代码
statusEl.textContent = '连接状态:你的浏览器不支持 SSE!';
statusEl.style.color = '#e63946';
  • 否则,通过open方法在连接成功时,对页面状态进行更改
  • 然后通过message来接收普通消息
js 复制代码
es.onmessage = (e) => {
    const data = JSON.parse(e.data);
    const item = document.createElement('div');
    item.className = 'normal';
    item.textContent = `[${data.time}] 随机数值:${data.value}`;
    normalDataEl.prepend(item);
    // 只保留最新 10 条
    if (normalDataEl.children.length > 10) {
        normalDataEl.removeChild(normalDataEl.lastChild);
    }
};
  • onmessage 事件:接收后端推送的默认事件(event: message)

  • e.data:后端推送的文本数据(JSON 字符串),需用 JSON.parse 转为 JavaScript 对象;

  • prepend(item):将新消息添加到父元素的最前面,实现 "最新消息置顶";

  • 条数限制:避免页面积累过多消息导致卡顿,只保留最新 10 条。

js 复制代码
// 接收自定义 alert 事件
es.addEventListener('alert', (e) => {
    const data = JSON.parse(e.data);
    const item = document.createElement('div');
    item.className = 'alert';
    item.textContent = `[${data.time}] ${data.msg}`;
    alertDataEl.prepend(item);
    // 只保留最新 5 条告警
    if (alertDataEl.children.length > 5) {
        alertDataEl.removeChild(alertDataEl.lastChild);
    }
});
  • addEventListener('alert', ...):监听后端推送的自定义事件(event: alert)

  • 区别于 onmessageonmessage 只能监听默认事件,自定义事件必须用 addEventListener

  • 条数限制:告警消息更重要,只保留最新 5 条,避免干扰。

js 复制代码
// 连接错误(error 事件)
es.onerror = (e) => {
    if (es.readyState === EventSource.CLOSED) {
        statusEl.textContent = '连接状态:连接已关闭(将自动重试)';
        statusEl.style.color = '#e9c46a';
    } else {
        statusEl.textContent = '连接状态:出错 - ' + (e.message || '未知错误');
        statusEl.style.color = '#e63946';
    }
};
  • onerror 事件:连接出错(如后端服务挂了、网络断开)时触发;

  • es.readyState:SSE 连接状态,EventSource.CLOSED 表示连接已关闭;

  • 逻辑:区分 "连接关闭(会自动重试)" 和 "未知错误",给出不同提示,提升用户体验。

js 复制代码
window.onbeforeunload = () => {
    es.close();
};
  • window.onbeforeunload:页面关闭 / 刷新前触发;

  • es.close():主动关闭 SSE 连接,避免后端长连接残留(后端会监听到 req.close 事件,清理定时器)。

效果

相关推荐
wuhen_n2 小时前
响应式探秘:ref vs reactive,我该选谁?
前端·javascript·vue.js
wuhen_n2 小时前
setup 的艺术:如何组织我们的组合式函数?
前端·javascript·vue.js
三翼鸟数字化技术团队2 小时前
前端架构演进与模块化设计实践
前端·架构
Moment2 小时前
Cursor 的 5 种指令方法比较,你最喜欢哪一种?
前端·后端·github
IT_陈寒2 小时前
Vite快得离谱?揭秘它比Webpack快10倍的5个核心原理
前端·人工智能·后端
明月_清风3 小时前
性能级目录同步:IntersectionObserver 实战
前端·javascript
明月_清风3 小时前
告别暴力轮询:深度解锁浏览器“观察者家族”
前端·javascript
摸鱼的春哥3 小时前
Agent教程17:LangChain的持久化和人工干预
前端·javascript·后端
程序员爱钓鱼4 小时前
Go操作Excel实战详解:github.com/xuri/excelize/v2
前端·后端·go