目录

WebSocket 不是唯一选择:SSE 打造轻量级实时推送系统 🚀🚀🚀

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

然后我们监听三个事件:

  1. onopen:连接成功,更新状态,重置重连次数。

  2. onmessage:收到数据,尝试解析 JSON,然后保存到状态里。

  3. onerror:连接失败,进入重连逻辑(详细见下面)。

当连接出错时,我们做了这些事:

  1. 断开当前连接

  2. 增加重连次数

  3. 用指数退避算法(越失败,重试间隔越长,最多 30 秒)

  4. 设置一个 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 设备状态推送等。它特别适合"只要服务器推就好"的场景,无需双向通信时是高效选择。

本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
inksci8 分钟前
watch 数组 Vue 3
前端·javascript·vue.js
2301_799404918 分钟前
深入解析 npm 与 Yarn:Node.js 包管理工具对比与选型指南
前端·npm·node.js
Clf丶忆笙31 分钟前
JavaScript性能优化实战:从基础到高级的全面指南
javascript·性能优化
前端没钱1 小时前
在Electron中爬取CSDN首页的文章信息
前端·javascript·爬虫·electron
RAY_CHEN.1 小时前
使用vue开发electron
前端·vue.js·electron
o不ok!2 小时前
Spark-小练试刀
开发语言·前端·javascript
朝阳392 小时前
Electron Forge【实战】带图片的 AI 聊天
javascript·人工智能·electron
Dontla2 小时前
npm命令介绍(Node Package Manager)
前端·npm·node.js
低级前端5 小时前
uniapp如何获取安卓原生的Intent对象
前端·uni-app·安卓·web app
渔舟唱晚@6 小时前
深度解析:Vue.js 性能优化全景指南(从原理到实践)
前端·vue.js·性能优化