目录​​​​​​​

前言

一、SSE 是什么?先理清定位

1. 核心定义

2. SSE vs WebSocket:选对场景更重要

二、SSE 的底层原理:HTTP 长连接 + 流式响应

1. 核心:HTTP 长连接的流式响应

2. 协议保障:浏览器原生解析

3. 稳定性保障:心跳 + 重连 + 断点续传

三、SSE 协议规范:必须遵守的 “规则”

1. 核心响应头

2. 消息格式:固定结构 + 空行结束

常见消息示例

四、从零实现 SSE:极简示例(Node.js + 原生 JS)

1. 服务端(Node.js):流式推送数据

2. 客户端(HTML + 原生 JS):实时接收

3. 运行效果

五、Spring Boot 实战 SSE:生产级实现

1. 基础依赖

2. SSE 连接管理器(核心)

3. 控制层接口

4. 前端测试

六、生产环境注意事项

1. Nginx 代理配置(关键)

2. 连接数优化

3. 异常处理

七、常见问题与避坑

总结


前言

在实时通信场景中,我们常听到 WebSocket,但很多人忽略了另一个轻量、易用的方案 ——SSE(Server-Sent Events)。相比于 WebSocket 的双向通信,SSE 专注于服务端向客户端单向实时推送,基于 HTTP 原生协议实现,无需复杂的协议升级和额外依赖,是系统通知、实时监控、日志推送等场景的最优解。

本文将从底层原理代码实现,一步步拆解 SSE 协议,带你彻底搞懂它的工作机制、协议规范和落地方式,新手也能跟着实操。

一、SSE 是什么?先理清定位

1. 核心定义

SSE(Server-Sent Events)是 HTML5 标准化的服务器向客户端单向实时推送数据的通信协议,基于 HTTP/HTTPS 协议实现,无需建立独立的 TCP 连接,专门解决 “HTTP 协议下服务端无法主动向客户端推送数据” 的问题。

2. SSE vs WebSocket:选对场景更重要

很多人会混淆两者,其实它们是互补而非替代关系,核心区别如下:

特性 SSE WebSocket
通信方向 单向(服务端→客户端) 双向(服务端↔客户端)
协议基础 基于 HTTP/HTTPS,复用现有栈 基于 TCP,需 HTTP 握手升级协议
重连机制 浏览器内置自动重连 需手动实现重连逻辑
开发复杂度 极低(原生 API + 简单协议) 较高(需处理握手、心跳)
部署成本 兼容所有 HTTP 网关 / 代理 需代理支持 WebSocket 升级
适用场景 实时通知、监控、日志推送 在线聊天、弹幕、实时协作

简单说:单向推送选 SSE,双向交互选 WebSocket

二、SSE 的底层原理:HTTP 长连接 + 流式响应

SSE 的 “实时性” 和 “单向推送” 特性,本质是对 HTTP 协议的巧妙利用,核心原理可拆解为 3 点:

1. 核心:HTTP 长连接的流式响应

普通 HTTP 请求是 “请求 - 响应” 模式:客户端发请求→服务端一次性返回所有数据→连接关闭;SSE 则是 “请求 - 持续响应” 模式:

  • 客户端发起一次 GET 请求后,服务端不立即关闭连接,保持HTTP 长连接
  • 服务端通过这个长连接,分块、实时输出数据(流式响应);
  • 浏览器实时监听响应流,每收到一块数据就立即解析,无需等待连接关闭。

2. 协议保障:浏览器原生解析

浏览器内置EventSource对象,专门用于处理 SSE 连接:

  • 自动识别text/event-stream类型的响应,无需手动解析流数据;
  • 内置自动重连、断点续传逻辑,无需开发者重复造轮子;
  • 按 SSE 协议格式解析消息,触发对应事件(如自定义事件、默认消息事件)。

3. 稳定性保障:心跳 + 重连 + 断点续传

为了保证长连接的稳定性,SSE 协议内置了 3 个关键机制:

  • 心跳保活:服务端定期发送注释消息(以:开头),防止长连接因 “空闲” 被网关 / 浏览器断开;
  • 自动重连:连接断开后,浏览器自动发起重连(默认 3 秒间隔,可自定义);
  • 断点续传:浏览器记录最后接收的消息 ID,重连时通过Last-Event-ID请求头告知服务端,服务端可补发未推送的消息。

三、SSE 协议规范:必须遵守的 “规则”

要实现 SSE,必须严格遵循协议规范,核心是响应头消息格式,这是浏览器能正确解析的前提。

1. 核心响应头

服务端返回的响应头必须包含以下字段,否则浏览器无法识别为 SSE 连接:

# 核心:标识SSE响应类型
Content-Type: text/event-stream; charset=UTF-8
# 禁止缓存,保证消息实时性
Cache-Control: no-cache
# 保持长连接
Connection: keep-alive
# Nginx代理时必须加:禁用缓冲区,避免消息被缓存
X-Accel-Buffering: no

2. 消息格式:固定结构 + 空行结束

SSE 的消息由 “字段行 + 空行” 组成,空行是消息结束的唯一标识,核心字段有 4 个:

字段 作用
data 消息体核心内容,支持多行(每行以data:开头,最终拼接为完整消息)
id 消息唯一标识,用于断点续传(浏览器记录为Last-Event-ID
event 自定义事件类型,客户端可按需监听(默认值为message
retry 浏览器自动重连的间隔(毫秒),覆盖默认值
常见消息示例
# 示例1:默认事件+单行数据
data: 这是一条普通消息
id: 1001

# 示例2:自定义事件+多行JSON数据
event: notice
id: 1002
data: {
data: "title": "系统通知",
data: "content": "订单支付成功"
data: }

# 示例3:注释消息(心跳保活,浏览器忽略)
: heartbeat-2026-01-29 16:00:00

# 示例4:自定义重连间隔
retry: 5000
data: 重连间隔设置为5秒

四、从零实现 SSE:极简示例(Node.js + 原生 JS)

为了让你直观理解,先实现一个最基础的 SSE 示例 —— 服务端每秒推送当前时间,客户端实时接收展示。

1. 服务端(Node.js):流式推送数据

新建server.js,无需任何框架,纯 Node.js 实现:

const http = require('http');

// 创建HTTP服务器
const server = http.createServer((req, res) => {
  // 仅处理/SSE路径的请求
  if (req.url === '/sse') {
    // 1. 设置SSE核心响应头
    res.writeHead(200, {
      'Content-Type': 'text/event-stream; charset=UTF-8',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
      'Access-Control-Allow-Origin': '*' // 解决跨域
    });

    // 2. 定时推送数据(每秒1次)
    let msgId = 1;
    const timer = setInterval(() => {
      const now = new Date().toLocaleString();
      // 按SSE协议拼接消息
      const sseMsg = `event: time
id: ${msgId}
data: 当前时间:${now}

`; // 空行必须加,标识消息结束
      
      // 3. 流式输出消息到客户端
      res.write(sseMsg);
      msgId++;
    }, 1000);

    // 4. 连接关闭时清理定时器,避免内存泄漏
    req.on('close', () => {
      clearInterval(timer);
      res.end();
    });
  } else {
    res.end('请访问 /sse 建立SSE连接');
  }
});

// 启动服务,监听3000端口
server.listen(3000, () => {
  console.log('SSE服务已启动:http://localhost:3000');
});

2. 客户端(HTML + 原生 JS):实时接收

新建index.html,直接打开即可:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>SSE极简示例</title>
</head>
<body>
  <h3>SSE实时接收时间戳:</h3>
  <div id="time-box" style="width: 600px; height: 300px; border: 1px solid #ccc; padding: 10px; overflow-y: auto;"></div>

  <script>
    // 1. 建立SSE连接
    const eventSource = new EventSource('http://localhost:3000/sse');

    // 2. 监听自定义事件(time)
    eventSource.addEventListener('time', (e) => {
      const timeBox = document.getElementById('time-box');
      // 3. 实时展示消息
      timeBox.innerHTML += `<p>消息ID:${e.lastEventId} | ${e.data}</p>`;
      // 滚动到底部
      timeBox.scrollTop = timeBox.scrollHeight;
    });

    // 连接成功回调
    eventSource.onopen = () => {
      console.log('SSE连接已建立');
    };

    // 连接异常回调(自动重连)
    eventSource.onerror = (e) => {
      console.log('连接异常,正在重连...', e);
    };

    // 页面关闭时主动关闭连接
    window.onbeforeunload = () => {
      eventSource.close();
    };
  </script>
</body>
</html>

3. 运行效果

  1. 执行node server.js启动服务;
  2. 打开index.html,页面会每秒刷新服务端推送的时间戳;
  3. 关闭服务端再重启,客户端会自动重连,继续接收数据。

五、Spring Boot 实战 SSE:生产级实现

如果你的技术栈是 Java/Spring Boot,这里提供生产级的 SSE 实现,包含连接管理、广播、心跳保活等核心功能。

1. 基础依赖

仅需 Spring Web 核心依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

2. SSE 连接管理器(核心)

用于管理所有客户端连接,避免内存泄漏:

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@Component
public class SseEmitterManager {
    // 存储客户端连接:key=客户端ID,value=SSE连接
    private final Map<String, SseEmitter> emitterMap = new ConcurrentHashMap<>();
    // 心跳线程池
    private ScheduledExecutorService heartbeatExecutor;
    // 心跳间隔(30秒)
    private static final long HEARTBEAT_INTERVAL = 30L;

    /**
     * 添加客户端连接
     */
    public SseEmitter addEmitter(String clientId) {
        SseEmitter emitter = new SseEmitter(30 * 60 * 1000L); // 30分钟超时
        // 连接关闭/异常时移除
        emitter.onCompletion(() -> emitterMap.remove(clientId));
        emitter.onError(e -> emitterMap.remove(clientId));
        emitter.onTimeout(() -> emitterMap.remove(clientId));
        emitterMap.put(clientId, emitter);
        return emitter;
    }

    /**
     * 向单个客户端推送消息
     */
    public void sendToClient(String clientId, String event, String id, Object data) {
        SseEmitter emitter = emitterMap.get(clientId);
        if (emitter == null) return;
        try {
            emitter.send(SseEmitter.event().name(event).id(id).data(data));
        } catch (Exception e) {
            emitterMap.remove(clientId);
        }
    }

    /**
     * 广播消息(推送给所有客户端)
     */
    public void broadcast(String event, String id, Object data) {
        emitterMap.forEach((clientId, emitter) -> sendToClient(clientId, event, id, data));
    }

    /**
     * 启动心跳保活任务
     */
    @PostConstruct
    public void init() {
        heartbeatExecutor = Executors.newSingleThreadScheduledExecutor();
        heartbeatExecutor.scheduleAtFixedRate(() -> {
            // 发送注释消息作为心跳
            emitterMap.forEach((clientId, emitter) -> {
                try {
                    emitter.send(": heartbeat");
                } catch (Exception e) {
                    emitterMap.remove(clientId);
                }
            });
        }, 0, HEARTBEAT_INTERVAL, TimeUnit.SECONDS);
    }

    /**
     * 销毁资源
     */
    @PreDestroy
    public void destroy() {
        if (heartbeatExecutor != null) heartbeatExecutor.shutdown();
        emitterMap.clear();
    }
}

3. 控制层接口

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.util.UUID;

@RestController
@RequestMapping("/api/sse")
@CrossOrigin(origins = "*") // 跨域配置
public class SseController {

    @Autowired
    private SseEmitterManager sseEmitterManager;

    /**
     * 建立SSE连接
     */
    @GetMapping("/connect")
    public SseEmitter connect(@RequestParam String clientId) {
        return sseEmitterManager.addEmitter(clientId);
    }

    /**
     * 向单个客户端推送消息
     */
    @PostMapping("/send")
    public void sendToClient(
            @RequestParam String clientId,
            @RequestParam String event,
            @RequestBody Object data
    ) {
        String msgId = UUID.randomUUID().toString();
        sseEmitterManager.sendToClient(clientId, event, msgId, data);
    }

    /**
     * 广播消息
     */
    @PostMapping("/broadcast")
    public void broadcast(@RequestParam String event, @RequestBody Object data) {
        String msgId = UUID.randomUUID().toString();
        sseEmitterManager.broadcast(event, msgId, data);
    }
}

4. 前端测试

复用第四部分的index.html,仅需修改 SSE 连接地址为:

const eventSource = new EventSource('http://localhost:8080/api/sse/connect?clientId=user1');

六、生产环境注意事项

1. Nginx 代理配置(关键)

若通过 Nginx 反向代理,需禁用缓冲区,否则消息会被缓存:

server {
    listen 80;
    server_name your-domain.com;

    location /api/sse/ {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Connection "keep-alive";
        proxy_set_header Host $host;
        proxy_cache off;
        proxy_buffering off; # 禁用缓冲区
        proxy_read_timeout 3600s; # 超时时间大于心跳间隔
    }
}

2. 连接数优化

浏览器默认单域名仅支持 6 个 SSE 连接,解决方案:

  • 多域名部署(如sse1.xxx.comsse2.xxx.com);
  • 前端使用共享 Worker,多个页面共用一个 SSE 连接。

3. 异常处理

  • 服务端:推送失败立即移除无效连接,设置超时时间;
  • 客户端:监听onerror事件,手动补充重连逻辑(配合浏览器自动重连)。

七、常见问题与避坑

  1. 消息延迟 / 不实时:大概率是 Nginx 开启了缓冲区,需添加proxy_buffering off
  2. 连接频繁断开:未设置心跳保活,服务端需定期发送注释消息;
  3. 重连后数据丢失:未使用id字段和Last-Event-ID实现断点续传;
  4. IE 浏览器不支持:IE 无EventSourceAPI,需使用 polyfill 库(如eventsource-polyfill)。

总结

SSE 协议的核心是基于 HTTP 长连接的流式响应,以 “轻量、易用、适配现有 HTTP 生态” 为核心优势,是单向实时推送场景的最佳选择:

  1. 原理层面:通过 HTTP 长连接 + 流式响应实现实时性,浏览器原生EventSource简化开发;
  2. 协议层面:严格的响应头和消息格式是正确解析的前提,空行是消息结束的关键;
  3. 实现层面:简单场景可直接用原生 API,生产环境需做好连接管理、心跳保活和异常处理。

如果你的业务场景是系统通知、实时监控、日志推送等单向推送需求,SSE 比 WebSocket 更简单、更易维护;如果需要双向交互,再考虑 WebSocket 即可。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐