在前端开发里,实现服务器和客户端高效通信很重要,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:内容nn”,两个连续换行表示一条数据结束。

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

四、nginx配置要点

如果在实际项目中使用SSE,涉及到nginx配置,有几个地方要注意。比如设置服务器监听端口、域名,还有处理反向代理时,要正确配置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:sseEventn`); // 给每个事件分配一个唯一标识符 res.write(`id:${index}n`); // 设置连接意外关闭后,客户端等待5秒再尝试重新连接 res.write(`retry: 5000n`); // 构建SSE消息,这里发送当前时间作为消息内容 res.write("data: " + JSON.stringify({ content: new Date() }) + "nn"); index++; console.log(index); }, 1000); // 当客户端关闭连接时,清除定时器,结束消息推送 req.on("close", () => { clearInterval(timer); res.end(); }); }); module.exports = router; 
  1. 整合到主应用:在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的原理、使用方法应该有了比较清晰的认识。如果在实际项目中需要更复杂的功能,比如更精准的消息控制、结合其他技术优化性能等,可以进一步探索和实践。