SSE服务器推送:实时网络传输协议详解与实战

1223 字
6 分钟
SSE服务器推送:实时网络传输协议详解与实战

本文基于作者实际开发经验,已在项目中使用 SSE 实现实时数据推送,涵盖客户端监听与服务端事件流实现。

SSE(Server-Sent Events)是基于HTTP的一个轻量级单向传输协议,允许服务器主动向客户端推送实时数据,场景主要有:新闻推送、消息通知、股票行情、实时日志等。

核心特性如下:

  1. 单向通信
  2. 基于HTTP
  3. 长连接(替代轮询)
  4. 自动重连

1.客户端基本使用方法#

这里简单画了个流程图表示生命周期:

  1. 先是连接成功触发open事件;
  2. 然后接收message消息是要配置监听的,建议用addEventListener,因为如果使用onMessage无法接收指定消息类型;
  3. 网络中断,服务器出错都会触发error事件;
  4. 关闭连接的close方法,通常离开页面就要关闭

SSE生命周期
SSE生命周期

//url为后端sse服务器地址,根据地址创建连接
const eventSource = new EventSource(url);
// 建立连接触发open事件
eventSource.onopen = () => {
console.log('✅ 触发open事件,SSE连接已建立');
};
// 方式1:使用onmessage属性
eventSource.onmessage = function (event) {
var data = event.data;
console.log('收到数据:', data);
};
// 方式2:使用addEventListener监听特定事件类型
eventSource.addEventListener('message', function (event) {
var data = event.data;
console.log('收到数据(监听方式):', data);
}, false);
// 异常触发error事件
eventSource.onerror = (error) => {
console.error('❌ 触发error事件,SSE连接错误:', error);
};
// 主动关闭SSE连接
eventSource.close();
console.log('SSE连接已手动关闭');

2.服务器端使用方法#

先要了解服务端的实现规范,主要从三个方面入手:HTTP头信息要求、数据传输格式、核心字段。

2.1 HTTP头信息要求#

Content-Type: text/event-stream // 必须,指定为事件流类型
Cache-Control: no-cache // 必须,禁止缓存,确保数据实时性
Connection: keep-alive // 必须,保持长连接

2.2 数据传输格式#

  1. 每行格式为字段: 值\n(字段名后必须跟冒号和空格,结尾用换行符\n)
  2. 多条消息之间用\n\n(两个换行符)分隔。
  3. 此外,以:开头的行是注释(服务器可定期发送注释保持连接)。

换行符必须是\n(Unix格式),\r\n可能导致客户端解析错误。

2.3 核心字段说明#

  • data字段:消息内容
  • event字段:指定事件类型
  • id字段:消息标识,发给谁
  • retry字段:重连间隔

3.服务端实现#

SSE在SpringBoot项目中,spring-boot-starter-web提供了SSE核心类SseEmitter

3.1 简单实现#

下面是一个简单的实现方式,创建一个接口供前端访客建立SSE长连接,然后提供了广播接口和模拟进度通知接口。

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@RestController
public class SseController {
private final CopyOnWriteArrayList<SseEmitter> emitters = new CopyOnWriteArrayList<>();
private final ExecutorService executor = Executors.newCachedThreadPool();
@GetMapping(value = "/sse/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter subscribe() {
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
emitters.add(emitter);
emitter.onCompletion(() -> emitters.remove(emitter));
emitter.onTimeout(() -> emitters.remove(emitter));
emitter.onError((e) -> emitters.remove(emitter));
try {
emitter.send(SseEmitter.event()
.name("CONNECTED")
.data("You are successfully connected to SSE server!")
.reconnectTime(5000));
} catch (IOException e) {
emitter.completeWithError(e);
}
return emitter;
}
@GetMapping("/sse/broadcast")
public String broadcastMessage(@RequestParam String message) {
executor.execute(() -> {
for (SseEmitter emitter : emitters) {
try {
emitter.send(SseEmitter.event()
.name("BROADCAST")
.data(message)
.id(String.valueOf(System.currentTimeMillis())));
} catch (IOException e) {
emitters.remove(emitter);
emitter.completeWithError(e);
}
}
});
return "Broadcast message: " + message;
}
@GetMapping("/sse/start-task")
public String startTask() {
executor.execute(() -> {
try {
for (int i = 0; i <= 100; i += 10) {
Thread.sleep(1000);
for (SseEmitter emitter : emitters) {
try {
emitter.send(SseEmitter.event()
.name("PROGRESS")
.data(i + "% completed")
.id("task-progress"));
} catch (IOException e) {
emitters.remove(emitter);
}
}
if (i == 100) {
for (SseEmitter emitter : emitters) {
try {
emitter.send(SseEmitter.event()
.name("COMPLETE")
.data("Task completed successfully!"));
} catch (IOException e) {
emitters.remove(emitter);
}
}
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
return "Task started!";
}
}

3.2 推荐实现#

推荐设计MessageEventTypeMessageEventSseEmitterManager三层架构:消息的事件类型枚举类、消息事件封装对象、SSE管理工具类。

限于篇幅,推荐实现的核心代码请参考原文中的 MessageEventType 枚举、MessageEvent 事件类和 SseEmitterManager 管理类。

4.客户端实现#

客户端使用React + antd-mobile实现消息列表,通过SSE接收实时消息推送,分为已读和未读消息。

hook.js 核心逻辑#

const createSSEConnection = (url, messageHandlers, setLoading) => {
const eventSource = new EventSource(url);
eventSource.onopen = () => {
console.log('✅ SSE连接已建立');
setLoading?.(false);
};
eventSource.onmessage = (event) => handleSSEMessage(event, messageHandlers);
Object.keys(messageHandlers).forEach(eventType => {
eventSource.addEventListener(eventType, (event) => {
try {
const data = JSON.parse(event.data);
messageHandlers[eventType](data);
} catch (error) {
console.error(`解析${eventType}失败:`, error);
}
});
});
eventSource.onerror = (error) => {
console.error('❌ SSE连接错误:', error);
setLoading?.(false);
};
return eventSource;
};

踩坑#

  1. 一定要注意后端发送的事件类型,和客户端监听的要保持一致,要不然客户端收不到消息。

事件类型对比1
事件类型对比1
事件类型对比2
事件类型对比2


相关推荐#

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

SSE服务器推送:实时网络传输协议详解与实战
https://blog.wmovie.site/posts/sse-protocol-guide/
作者
朵朵
发布于
2025-11-03
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
朵朵
全栈开发者,5 年 Java/SpringBoot + Vue/React 开发经验,曾维护 Delphi 遗留系统。业余深耕 NAS 与软路由,专注 Homelab 家庭网络搭建及 Docker 自托管应用实战。坐标福州,正迈向独立开发之路。
公告
欢迎来到我的博客!有什么想要的邮箱告诉我,最近沉迷nas和软路由。
分类
标签
站点统计
文章
25
分类
4
标签
18
总字数
32,908
运行时长
0
最后活动
0 天前

文章目录