什么是服务器发送事件

什么是服务器发送事件
- 基本概念与工作原理
- 客户端 EventSource API 详解
- 服务端 SSE 响应格式与实现
- 高级用法与工程化实践
- 与现代前端框架集成
- 安全性与部署考量
在现代 Web 前端知识开发体系中,实时通信技术是构建动态、响应式用户界面的核心支柱。随着单页应用(SPA)和富交互体验的普及,传统的客户端轮询(Polling)与短轮询(Short Polling)机制已无法满足低延迟、高并发的实时数据更新需求。在此背景下,HTML5 引入的服务器发送事件(Server-Sent Events,简称 SSE)作为一种轻量级、标准化的单向实时通信协议,为前端开发者提供了高效、简洁的解决方案。SSE 基于 HTTP 长连接,允许服务器主动向客户端推送文本数据流,特别适用于股票行情、新闻推送、日志监控、实时通知等场景。
SSE 的核心优势在于其简单性与标准化:它无需引入额外的协议(如 WebSocket 的 WS/WSS),复用现有 HTTP 基础设施,自动处理连接断开与重连,并通过 EventSource API 提供统一的 JavaScript 接口。与 WebSocket 相比,SSE 实现更轻量,浏览器兼容性更广,且天然支持跨域(CORS)与代理服务器。理解 SSE 的工作原理、协议规范、API 使用模式及工程化实践,是现代前端开发者构建实时系统的必备技能。
基本概念与工作原理
服务器发送事件(SSE)是一种基于 HTTP 的服务器推送技术,其通信模型为单向:服务器 → 客户端。客户端通过 EventSource 接口建立与服务器的持久连接,服务器则以特定的 MIME 类型 text/event-stream 持续发送 UTF-8 编码的文本数据。连接一旦建立,除非显式关闭或网络中断,否则将保持打开状态,服务器可随时推送新数据。
SSE 协议规定了严格的消息格式,每条消息由若干字段组成,字段间以换行符
分隔,消息间以双换行符
分隔。支持的字段包括:
data:消息正文,可跨行,以空行结尾event:自定义事件类型,供客户端监听id:消息唯一标识,用于断线重连时定位retry:重连间隔(毫秒),建议服务器设置
当连接意外中断时,EventSource 会自动尝试重连,默认延迟为 3 秒(可由服务器通过 retry 字段指定)。重连时,浏览器会发送 Last-Event-ID 请求头,告知服务器最后接收到的消息 ID,服务器可据此恢复推送流,避免数据丢失。
客户端 EventSource API 详解
EventSource 是浏览器原生提供的接口,用于创建与 SSE 服务器的连接。其构造函数接收一个 URL 参数,并可选配置对象。
// 示例一:基础 EventSource 连接与默认事件监听
const eventSource = new EventSource('https://api.example.com/events');
// 监听 open 事件:连接成功建立
eventSource.addEventListener('open', (event) => {
console.log('SSE 连接已打开');
console.log('当前就绪状态:', eventSource.readyState); // 1 (OPEN)
});
// 监听 message 事件:接收默认类型消息(无 event 字段)
eventSource.addEventListener('message', (event) => {
try {
// 解析服务器发送的 JSON 数据
const data = JSON.parse(event.data);
console.log('收到消息:', data);
console.log('消息ID:', event.lastEventId); // 对应服务器的 id 字段
// 更新 UI,例如:更新通知面板
updateNotificationPanel(data);
} catch (err) {
console.error('消息解析失败:', err);
}
});
// 监听 error 事件:连接错误或重连失败
eventSource.addEventListener('error', (event) => {
console.error('SSE 连接错误:', event);
// readyState: 0 (CONNECTING), 1 (OPEN), 2 (CLOSED)
console.log('当前状态:', eventSource.readyState);
if (eventSource.readyState === EventSource.CLOSED) {
console.warn('连接已关闭,需手动重连');
}
// 注意:error 事件不表示连接永久失败,EventSource 会自动重连
});
EventSource 支持三种就绪状态常量:CONNECTING (0)、OPEN (1)、CLOSED (2)。开发者可通过 eventSource.readyState 查询当前状态。addEventListener 方法支持监听任意自定义事件类型,实现消息的分类处理。
// 示例二:监听自定义事件类型
// 假设服务器推送不同类型的通知
eventSource.addEventListener('news', (event) => {
const news = JSON.parse(event.data);
displayNewsFlash(news);
});
eventSource.addEventListener('alert', (event) => {
const alert = JSON.parse(event.data);
triggerSystemAlert(alert);
});
eventSource.addEventListener('heartbeat', (event) => {
// 心跳包用于检测连接活性
console.log('心跳:', event.data);
resetInactivityTimer();
});
服务端 SSE 响应格式与实现
服务器必须设置正确的响应头,并持续输出符合 SSE 协议的文本流。以下以 Node.js + Express 为例展示服务端实现。
// 示例三:Node.js Express 服务端 SSE 实现
const express = require('express');
const app = express();
// 中间件:设置通用响应头
app.use((req, res, next) => {
// 启用 CORS,允许前端域名访问
res.setHeader('Access-Control-Allow-Origin', 'https://frontend.example.com');
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
});
// SSE 路由:/events
app.get('/events', (req, res) => {
// 设置 SSE 必需的响应头
res.writeHead(200, {
'Content-Type': 'text/event-stream', // 关键:声明事件流
'Cache-Control': 'no-cache', // 禁止缓存
'Connection': 'keep-alive', // 保持长连接
'X-Accel-Buffering': 'no', // Nginx:禁用缓冲
'Access-Control-Allow-Origin': 'https://frontend.example.com',
'Access-Control-Allow-Credentials': 'true'
});
// 可选:设置初始重连间隔
res.write('retry: 10000
'); // 10秒重连
// 模拟定期发送消息
const intervalId = setInterval(() => {
const now = new Date().toISOString();
// 发送默认消息(data 字段)
res.write(`data: {"time": "${now}", "value": ${Math.random()}}
`);
}, 3000);
// 发送带 ID 和事件类型的消息
const newsInterval = setInterval(() => {
const newsId = Date.now();
res.write(`id: news-${newsId}
`); // 消息ID
res.write(`event: news
`); // 事件类型
res.write(`data: {"title": "突发新闻", "content": "实时事件更新"}
`);
}, 15000);
// 发送心跳包
const heartbeat = setInterval(() => {
res.write(`event: heartbeat
`);
res.write(`data: ping
`);
}, 30000);
// 监听客户端断开连接
req.on('close', () => {
console.log('客户端断开连接');
clearInterval(intervalId);
clearInterval(newsInterval);
clearInterval(heartbeat);
res.end(); // 清理资源
});
// 初始欢迎消息
res.write(`data: {"welcome": "连接已建立"}
`);
});
关键响应头说明:
Content-Type: text/event-stream:告知浏览器此为事件流,触发EventSource解析逻辑。Cache-Control: no-cache:防止代理或浏览器缓存响应。Connection: keep-alive:明确保持 TCP 连接。X-Accel-Buffering: no:Nginx 特有指令,禁用响应缓冲,确保数据即时推送。
高级用法与工程化实践
在复杂应用中,SSE 需结合状态管理、错误恢复与性能优化策略。
// 示例四:增强型 EventSource 封装类
class ReliableEventSource {
constructor(url, options = {}) {
this.url = url;
this.options = options;
this.eventSource = null;
this.reconnectInterval = options.reconnectInterval || 5000;
this.maxReconnectAttempts = options.maxReconnectAttempts || null;
this.reconnectAttempts = 0;
this.listeners = new Map(); // 存储事件监听器
this.isOpen = false;
this.connect();
}
connect() {
// 包含查询参数以支持服务端识别
const urlWithParams = new URL(this.url, window.location.origin);
if (this.options.token) {
urlWithParams.searchParams.append('token', this.options.token);
}
this.eventSource = new EventSource(urlWithParams.href, {
withCredentials: true // 发送凭据(cookies)
});
this.eventSource.addEventListener('open', this.onOpen.bind(this));
this.eventSource.addEventListener('message', this.onMessage.bind(this));
this.eventSource.addEventListener('error', this.onError.bind(this));
// 绑定所有已注册的自定义事件监听器
this.listeners.forEach((handlers, eventType) => {
handlers.forEach(handler => {
this.eventSource.addEventListener(eventType, handler);
});
});
}
onOpen(event) {
console.log('可靠 SSE 连接已打开');
this.isOpen = true;
this.reconnectAttempts = 0; // 重置重连计数
// 通知所有监听器
this.dispatch('sse:connected', event);
}
onMessage(event) {
// 预处理消息,添加元数据
const enrichedEvent = {
...event,
timestamp: Date.now(),
source: this.url
};
this.dispatch('message', enrichedEvent);
}
onError(event) {
console.error('SSE 错误:', event);
this.isOpen = false;
this.dispatch('sse:error', event);
// 自动重连逻辑
if (this.shouldReconnect()) {
setTimeout(() => {
console.log(`第 ${++this.reconnectAttempts} 次重连尝试...`);
this.connect();
}, this.reconnectInterval);
} else {
console.warn('达到最大重连次数,停止重连');
this.dispatch('sse:disconnected', event);
}
}
shouldReconnect() {
if (this.maxReconnectAttempts === null) return true;
return this.reconnectAttempts < this.maxReconnectAttempts;
}
// 注册事件监听器
addEventListener(eventType, handler) {
if (!this.listeners.has(eventType)) {
this.listeners.set(eventType, []);
}
this.listeners.get(eventType).push(handler);
// 若连接已建立,立即绑定到 EventSource
if (this.eventSource && this.isOpen) {
this.eventSource.addEventListener(eventType, handler);
}
}
// 移除事件监听器
removeEventListener(eventType, handler) {
if (this.listeners.has(eventType)) {
const handlers = this.listeners.get(eventType);
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
if (this.eventSource) {
this.eventSource.removeEventListener(eventType, handler);
}
}
}
}
// 派发内部事件
dispatch(eventType, detail) {
const event = new CustomEvent(eventType, { detail });
window.dispatchEvent(event);
}
// 显式关闭连接
close() {
if (this.eventSource) {
this.eventSource.close();
this.isOpen = false;
console.log('SSE 连接已手动关闭');
}
}
}
// 使用封装类
const reliableSSE = new ReliableEventSource('https://api.example.com/events', {
token: 'user-jwt-token',
reconnectInterval: 3000,
maxReconnectAttempts: 10
});
reliableSSE.addEventListener('news', (event) => {
console.log('增强型接收:', event.data);
});
// 监听连接状态变化
window.addEventListener('sse:connected', (e) => {
showStatusBadge('online');
});
window.addEventListener('sse:error', (e) => {
showStatusBadge('reconnecting');
});
与现代前端框架集成
在 React、Vue 等框架中使用 SSE,需注意组件生命周期与状态同步。
// 示例五:React Hook 封装 useSSE
import { useState, useEffect, useCallback } from 'react';
function useSSE(url, eventHandlers, dependencies = []) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [connected, setConnected] = useState(false);
const [lastEventId, setLastEventId] = useState(null);
const createEventHandler = useCallback((setData, setError, setConnected, setLastEventId) => {
return {
onOpen: () => {
setConnected(true);
setError(null);
},
onMessage: (event) => {
try {
const parsedData = JSON.parse(event.data);
setData(parsedData);
setLastEventId(event.lastEventId);
} catch (err) {
console.error('SSE 数据解析失败:', err);
setError(err);
}
},
onError: (event) => {
setError(event);
setConnected(false);
}
};
}, []);
useEffect(() => {
const eventSource = new EventSource(url);
const handlers = createEventHandler(setData, setError, setConnected, setLastEventId);
eventSource.addEventListener('open', handlers.onOpen);
eventSource.addEventListener('message', handlers.onMessage);
eventSource.addEventListener('error', handlers.onError);
// 绑定自定义事件处理器
Object.keys(eventHandlers).forEach(eventType => {
eventSource.addEventListener(eventType, eventHandlers[eventType]);
});
// 清理函数:组件卸载时关闭连接
return () => {
eventSource.removeEventListener('open', handlers.onOpen);
eventSource.removeEventListener('message', handlers.onMessage);
eventSource.removeEventListener('error', handlers.onError);
Object.keys(eventHandlers).forEach(eventType => {
eventSource.removeEventListener(eventType, eventHandlers[eventType]);
});
eventSource.close();
};
}, [url, ...dependencies]); // 依赖项变化时重建连接
return { data, error, connected, lastEventId };
}
// 在组件中使用
function StockTicker({ symbol }) {
const { data, connected } = useSSE(
`https://api.stock.com/price?symbol=${symbol}`,
{
'price-update': (event) => {
console.log('价格更新:', event.data);
// 可触发额外业务逻辑
}
},
[symbol] // symbol 变化时重建 SSE 连接
);
if (!connected) return <div>连接中...</div>;
if (!data) return <div>加载中...</div>;
return (
<div>
<h3>{symbol}: ${data.price.toFixed(2)}</h3>
<small>更新时间: {new Date(data.timestamp).toLocaleTimeString()}</small>
</div>
);
}
安全性与部署考量
生产环境中使用 SSE 需关注安全性与可扩展性:
- 认证与授权:通过 URL 查询参数(如
?token=...)或withCredentials: true发送 Cookie 实现身份验证。服务端需验证每次连接的合法性。 - 连接限制:每个客户端一个连接,避免滥用。服务端应设置合理的超时(如 60-300 秒)并监控连接数。
- 代理配置:Nginx 需禁用缓冲并调整超时:
location /events { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_buffering off; proxy_cache off; proxy_read_timeout 3600s; # 长超时 proxy_send_timeout 3600s; } - 降级策略:对于不支持
EventSource的旧浏览器(如 IE),可降级至长轮询(Long Polling)或使用 polyfill。 - 消息格式:推荐使用 JSON 格式化
data字段,便于前后端数据交换。避免发送大量文本,防止内存溢出。
在微服务架构中,SSE 端点常作为 BFF(Backend For Frontend)层的一部分,聚合多个后端服务的数据并推送至前端。结合 Redis Pub/Sub 或消息队列(如 Kafka),可实现跨服务的事件广播,确保数据一致性与实时性。
欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
推荐:DTcode7的博客首页。
一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!
专栏系列(点击解锁) 学习路线(点击解锁) 知识定位 《微信小程序相关博客》 持续更新中~ 结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等 《AIGC相关博客》 持续更新中~ AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结 《HTML网站开发相关》 《前端基础入门三大核心之html相关博客》 前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识 《前端基础入门三大核心之JS相关博客》 前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。
通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心《前端基础入门三大核心之CSS相关博客》 介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页 《canvas绘图相关博客》 Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化 《Vue实战相关博客》 持续更新中~ 详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅 《python相关博客》 持续更新中~ Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具 《sql数据库相关博客》 持续更新中~ SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能 《算法系列相关博客》 持续更新中~ 算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维 《IT信息技术相关博客》 持续更新中~ 作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识 《信息化人员基础技能知识相关博客》 无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方 《信息化技能面试宝典相关博客》 涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面 《前端开发习惯与小技巧相关博客》 持续更新中~ 罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等 《photoshop相关博客》 持续更新中~ 基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结 日常开发&办公&生产【实用工具】分享相关博客》 持续更新中~ 分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具
吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤
非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!


