面试导航 是一个专注于前、后端技术学习和面试准备的 免费 学习平台,提供系统化的技术栈学习,深入讲解每个知识点的核心原理,帮助开发者构建全面的技术体系。平台还收录了大量真实的校招与社招面经,帮助你快速掌握面试技巧,提升求职竞争力。如果你想加入我们的交流群,欢迎通过微信联系:
yunmz777
。

在需要服务器实时向浏览器推送数据 的场景中,很多人第一反应是使用 WebSocket,但其实还有一种更轻量、更简单的解决方案 ------ SSE(Server-Sent Events) 。它天生适合"服务器单向推送",而且浏览器原生支持、无需额外协议、写起来极其简单。
本文将从原理、协议、代码、对比、性能、安全等多个方面,帮你系统了解 SSE 的底层机制与实际应用。
🧠 一、什么是 SSE?
SSE,全称 Server-Sent Events,是 HTML5 提出的标准之一,用于建立一种 客户端到服务器的持久连接 ,允许服务器在数据更新时,主动将事件推送到客户端。
通俗点讲,它就像是:
浏览器发起了一个请求,服务器就打开一个"水管",源源不断地往客户端输送数据流,直到你手动关闭它。
它基于标准的 HTTP 协议,与传统请求-响应的"短连接"模式不同,SSE 是长连接,并且保持活跃,类似于"实时通知通道"。
🛠️ 二、SSE 的通信机制与协议细节
✅ 客户端:使用 EventSource
建立连接
javascript
const sse = new EventSource("/events");
sse.onmessage = (event) => {
console.log("新消息:", event.data);
};
EventSource 是浏览器自带的,直接用就行,不用装库。它会自动处理连接、断线重连这些问题,基本不需要你操心,消息来了就能收到。
原生 EventSource 的使用限制
虽然原生的 EventSource
对象很方便,但也存在很多的限制,它只能发送 GET 请求,不支持设置请求方法,也不能附带请求体。
你不能通过 EventSource 设置如 Authorization、token 等自定义请求头用于鉴权。
例如,下面这样是不被支持的:
js
const sse = new EventSource("/events", {
headers: {
Authorization: "Bearer xxx",
},
});
这在 fetch 里没问题,但在 EventSource 里完全不支持。直接报错,浏览器压根不给你设置 headers。
EventSource 虽然支持跨域,但得服务器配合设置 CORS,而且还不能用 withCredentials。换句话说,你不能让它自动带上 cookie,那些基于 cookie 登录的服务就麻烦了。
如果你需要传 token 或做鉴权,可以使用查询参数传 token,比如这样:
js
const token = "abc123";
const sse = new EventSource(`/events?token=${token}`);
✅ 服务器:响应格式必须为 text/event-stream
服务器需要返回特定格式的数据流,并设置以下 HTTP 响应头:
http
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
如下图所示:

然后每条消息遵循下面的格式:
vbnet
data: Hello from server
id: 1001
event: message
如下图所示:
在上面的内容中,主要有以下解释,如下表格所示:
字段 | 说明 |
---|---|
data: |
消息正文内容,支持多行 |
id: |
消息 ID,浏览器断线重连后会通过 Last-Event-ID 自动恢复 |
event: |
自定义事件名(默认是 message ) |
retry: |
指定断线重连间隔(毫秒) |
🔄 三、SSE vs WebSocket vs 轮询,对比总结
特性 | SSE | WebSocket | 长轮询(Ajax) |
---|---|---|---|
通信方向 | 单向(服务器 → 客户端) | 双向 | 单向 |
协议 | HTTP | 自定义 ws 协议 | HTTP |
支持断线重连 | ✅ 内置自动重连 | ❌ 需手动重连逻辑 | ❌ |
浏览器兼容性 | 现代浏览器支持,IE 不支持 | 广泛支持 | 兼容性强 |
复杂度 | ✅ 最简单,零依赖 | 中等 | 简单但消耗高 |
使用场景 | 实时通知、进度、新闻、后台日志 | 聊天、游戏、协作、股票交易等 | 简单刷新类数据 |
🚀 四:如何在 NextJs 中实现
NextJS 作为一个现代化的 React 框架,非常适合实现 SSE。下面我们将通过一个完整的实例来展示如何在 NextJS 应用中实现服务器发送事件。
前端代码如下:
tsx
"use client";
import React, { useState, useEffect, useRef } from "react";
export default function SSEDemo() {
const [sseData, setSseData] = useState<{
time?: string;
value?: string;
message?: string;
error?: string;
} | null>(null);
const [connected, setConnected] = useState(false);
const [reconnecting, setReconnecting] = useState(false);
const [reconnectCount, setReconnectCount] = useState(0);
const eventSourceRef = useRef<EventSource | null>(null);
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// 建立SSE连接
const connectSSE = () => {
// 关闭任何现有连接
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
// 清除任何挂起的重连计时器
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}
try {
setReconnecting(true);
// 添加时间戳防止缓存
const eventSource = new EventSource(`/api/sse?t=${Date.now()}`);
eventSourceRef.current = eventSource;
eventSource.onopen = () => {
setConnected(true);
setReconnecting(false);
setReconnectCount(0);
console.log("SSE连接已建立");
};
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
setSseData(data);
} catch (error) {
console.error("解析SSE数据失败:", error);
}
};
eventSource.onerror = (error) => {
console.error("SSE连接错误:", error);
setConnected(false);
eventSource.close();
// 增加重连次数
setReconnectCount((prev) => prev + 1);
// 随着失败次数增加,增加重连间隔(指数退避策略)
const reconnectDelay = Math.min(
30000,
1000 * Math.pow(2, Math.min(reconnectCount, 5))
);
setReconnecting(true);
setSseData((prev) => ({
...prev,
message: `连接失败,${reconnectDelay / 1000}秒后重试...`,
}));
// 尝试重新连接
reconnectTimeoutRef.current = setTimeout(() => {
connectSSE();
}, reconnectDelay);
};
} catch (error) {
console.error("创建SSE连接失败:", error);
setConnected(false);
setReconnecting(true);
// 5秒后重试
reconnectTimeoutRef.current = setTimeout(() => {
connectSSE();
}, 5000);
}
};
useEffect(() => {
connectSSE();
// 定期检查连接是否健康
const healthCheck = setInterval(() => {
if (eventSourceRef.current && !connected) {
// 如果存在连接但状态是未连接,尝试重新连接
connectSSE();
}
}, 30000);
// 清理函数
return () => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}
clearInterval(healthCheck);
};
}, []);
return (
<div className="min-h-screen bg-gradient-to-b from-slate-900 to-slate-800 text-white flex flex-col items-center justify-center p-4">
<div className="w-full max-w-md bg-slate-800 rounded-xl shadow-2xl overflow-hidden">
<div className="p-6 border-b border-slate-700">
<h1 className="text-3xl font-bold text-center text-blue-400">
SSE 演示
</h1>
<div className="mt-2 flex items-center justify-center">
<div
className={`h-3 w-3 rounded-full mr-2 ${
connected
? "bg-green-500"
: reconnecting
? "bg-yellow-500 animate-pulse"
: "bg-red-500"
}`}
></div>
<p className="text-sm text-slate-300">
{connected
? "已连接到服务器"
: reconnecting
? `正在重新连接 (尝试 ${reconnectCount})`
: "连接断开"}
</p>
</div>
{!connected && (
<button
onClick={() => connectSSE()}
className="mt-3 px-3 py-1 bg-blue-600 text-sm text-white rounded-md mx-auto block hover:bg-blue-700"
>
手动重连
</button>
)}
</div>
{sseData && (
<div className="p-6">
{sseData.error ? (
<div className="rounded-lg bg-red-900/30 p-4 mb-4 text-center border border-red-800">
<p className="text-lg text-red-300">{sseData.error}</p>
</div>
) : sseData.message ? (
<div className="rounded-lg bg-slate-700 p-4 mb-4 text-center">
<p className="text-lg text-blue-300">{sseData.message}</p>
</div>
) : (
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-slate-400">时间:</span>
<span className="font-mono bg-slate-700 px-3 py-1 rounded-md text-blue-300">
{sseData.time &&
new Date(sseData.time).toLocaleTimeString()}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-slate-400">随机值:</span>
<span className="font-mono bg-slate-700 px-3 py-1 rounded-md text-green-300">
{sseData.value}
</span>
</div>
</div>
)}
</div>
)}
{!sseData && (
<div className="p-6 text-center text-slate-400">
<p>等待数据中...</p>
<div className="mt-4 flex justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-400"></div>
</div>
</div>
)}
</div>
</div>
);
}
在上面的代码中,我们用的是浏览器的原生 EventSource,加了个时间戳 t=${Date.now()}
是为了防止缓存,确保每次都是新的连接。
然后我们监听三个事件:
-
onopen:连接成功,更新状态,重置重连次数。
-
onmessage:收到数据,尝试解析 JSON,然后保存到状态里。
-
onerror:连接失败,进入重连逻辑(详细见下面)。
当连接出错时,我们做了这些事:
-
断开当前连接
-
增加重连次数
-
用指数退避算法(越失败,重试间隔越长,最多 30 秒)
-
设置一个 setTimeout 自动重连
而且页面上也有提示「正在重连」和「手动重连」的按钮,体验很人性化。
接下来我们看看后端代码,如下:
ts
export async function GET() {
// 标记连接是否仍然有效,
let connectionClosed = false;
// 使用Next.js的流式响应处理
return new Response(
new ReadableStream({
start(controller) {
const encoder = new TextEncoder();
// 监测响应对象是否被关闭
const abortController = new AbortController();
const signal = abortController.signal;
signal.addEventListener("abort", () => {
connectionClosed = true;
cleanup();
});
// 安全发送数据函数
const safeEnqueue = (data: string) => {
if (connectionClosed) return;
try {
controller.enqueue(encoder.encode(data));
} catch (error) {
console.error("SSE发送错误:", error);
connectionClosed = true;
cleanup();
}
};
// 发送初始数据
safeEnqueue(`data: ${JSON.stringify({ message: "连接已建立" })}\n\n`);
// 定义interval引用
let heartbeatInterval: NodeJS.Timeout | null = null;
let dataInterval: NodeJS.Timeout | null = null;
// 清理所有资源
const cleanup = () => {
if (heartbeatInterval) clearInterval(heartbeatInterval);
if (dataInterval) clearInterval(dataInterval);
// 尝试安全关闭控制器
try {
if (!connectionClosed) {
controller.close();
}
} catch (e) {
// 忽略关闭时的错误
}
};
// 设置10秒的心跳间隔,避免连接超时
heartbeatInterval = setInterval(() => {
if (connectionClosed) {
cleanup();
return;
}
safeEnqueue(": heartbeat\n\n");
}, 10000);
// 每秒发送一次数据
dataInterval = setInterval(() => {
if (connectionClosed) {
cleanup();
return;
}
try {
const data = {
time: new Date().toISOString(),
value: Math.random().toFixed(3),
};
safeEnqueue(`data: ${JSON.stringify(data)}\n\n`);
} catch (error) {
console.error("数据生成错误:", error);
connectionClosed = true;
cleanup();
}
}, 1000);
// 60秒后自动关闭连接(可根据需要调整)
setTimeout(() => {
// 只有当连接仍然活跃时才发送消息和关闭
if (!connectionClosed) {
try {
safeEnqueue(
`data: ${JSON.stringify({
message: "连接即将关闭,请刷新页面重新连接",
})}\n\n`
);
connectionClosed = true;
cleanup();
} catch (e) {
// 忽略关闭时的错误
}
}
}, 60000);
},
cancel() {
// 当流被取消时调用
connectionClosed = true;
},
}),
{
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
"Access-Control-Allow-Origin": "*",
"X-Accel-Buffering": "no", // 适用于某些代理服务器如Nginx
},
}
);
}
这段代码是 Next.js 后端 API 路由,用来实现 SSE(Server-Sent Events)长连接。我们使用了 ReadableStream
创建一个持续向前端推送数据的响应流,并配合 AbortSignal
检测连接是否被关闭:
ts
return new Response(new ReadableStream({ start(controller) { ... } }), { headers: {...} });
一开始,服务器通过 safeEnqueue
安全地向客户端发送一条欢迎消息:
ts
safeEnqueue(`data: ${JSON.stringify({ message: "连接已建立" })}\n\n`);
随后每秒生成一条数据(当前时间和随机值)推送给前端,并通过 setInterval
定时发送:
ts
const data = {
time: new Date().toISOString(),
value: Math.random().toFixed(3),
};
safeEnqueue(`data: ${JSON.stringify(data)}\n\n`);
为了保持连接活跃,避免浏览器或代理中断连接,我们每 10 秒发送一次心跳包(以冒号开头的注释):
ts
safeEnqueue(": heartbeat\n\n");
还加了一个自动关闭机制------60 秒后主动断开连接并提示前端刷新:
ts
safeEnqueue(
`data: ${JSON.stringify({ message: "连接即将关闭,请刷新页面重新连接" })}\n\n`
);
整个数据发送过程都包裹在 safeEnqueue
中,确保连接断开时能安全终止,并调用 cleanup()
清理资源。响应头中我们指定了 text/event-stream
,关闭了缓存,并设置了必要的长连接参数:
ts
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
"Access-Control-Allow-Origin": "*",
"X-Accel-Buffering": "no"
}
通过这种方式,服务端可以稳定地向客户端发送实时数据,同时具备自动断开、心跳维持、错误处理等健壮性,是非常实用的 SSE 实践方案。
最终结果如下图所示:

成功实现。
总结
SSE(Server-Sent Events)是一种基于 HTTP 的 服务器向客户端单向推送数据的机制 ,适用于需要持续更新前端状态的场景。除了浏览器原生支持的 EventSource
,也可以通过 fetch + ReadableStream
或框架内置流式处理(如 Next.js API Route、Node.js Response Stream)来实现,适配更复杂或自定义需求。相比 WebSocket,SSE 实现更简单,自动断线重连、无需维护双向协议,非常适合实时消息通知、进度条更新、在线人数统计、系统日志流、IoT 设备状态推送等。它特别适合"只要服务器推就好"的场景,无需双向通信时是高效选择。