前后端流式传输技术详解
在现代Web应用开发中,实时数据传输已成为提升用户体验的关键技术。本文将详细介绍四种主流的前后端流式传输方式:SseEmitter、ResponseBodyEmitter、StreamingResponseBody和WebSocket,并分析各自的特点、适用场景以及实现方式。
一、Server-Sent Events (SSE) 与 SseEmitter
1.1 SSE 技术简介
Server-Sent Events (SSE) 是一种基于HTTP的服务器推送技术,允许服务器向客户端发送事件流。与WebSocket不同,SSE是单向通信的,仅支持服务器向客户端推送数据。
SSE的主要特点:
- 基于HTTP协议,无需额外的协议支持
- 自动重连机制
- 支持自定义事件
- 单向通信(服务器→客户端)
- 文本格式传输
1.2 SseEmitter 实现
Spring框架提供了SseEmitter
类来实现SSE功能。
后端代码
@GetMapping("/T1")
SseEmitter T1() {
SseEmitter emitter = new SseEmitter();
new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
emitter.send(SseEmitter.event().id(String.valueOf(i))
.name("msg").data("sse" + i));
Thread.sleep(100);
}
emitter.complete();
} catch (Exception e) { emitter.completeWithError(e);}
}).start();
return emitter;
}
技术要点:
SseEmitter
类创建了一个SSE连接emitter.send()
方法用于发送事件,可设置事件ID、名称和数据emitter.complete()
表示传输完成- 实际应用中通常在新线程中处理数据发送,避免阻塞主线程
前端代码
// 创建EventSource对象连接到服务器端点
const source = new EventSource('/api/test');
// 连接成功的回调
source.onmessage = function(event) {
console.log('接收到消息:', event.data);
// 处理接收到的数据
}
// 监听特定事件(如果后端发送了命名事件)
source.addEventListener("msg", function(event) {
console.log('接收到msg事件:', event.data);
}, false);
// 处理错误
source.addEventListener("error", function(err) {
console.error('发生错误:', err);
// 可以根据错误状态码处理不同情况
err && err.status === 401 && console.log('未授权');
})
// 关闭连接
// source.close();
技术要点:
EventSource
是浏览器原生支持的SSE客户端API- 支持自动重连(除非连接返回HTTP 204)
- 可以监听命名事件或默认消息
- 需要手动关闭连接
1.3 适用场景
SSE特别适合以下场景:
- 实时通知和提醒
- 实时数据更新(如股票价格、天气更新)
- 社交媒体信息流
- 日志或事件监控
- AI聊天应用中的流式响应
二、ResponseBodyEmitter
2.1 技术简介
ResponseBodyEmitter
是Spring框架提供的一个用于异步HTTP响应的组件,允许在一个HTTP请求中多次写入响应数据。与SSE不同,它不遵循特定的事件格式,而是发送原始数据流。
2.2 实现方式
后端代码
@GetMapping("/T2")
ResponseBodyEmitter T2() {
ResponseBodyEmitter emitter = new ResponseBodyEmitter();
new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
emitter.send("res" + i);
// 模拟数据生成时间
Thread.sleep(100);
}
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
}).start();
return emitter;
}
技术要点:
ResponseBodyEmitter
提供了低级别的响应控制- 可以发送任何类型的数据,不限于文本
- 支持分块传输编码(chunked transfer encoding)
- 需要手动处理完成和错误情况
2.3 适用场景
ResponseBodyEmitter
适用于:
- 大型数据集的分块传输
- 需要自定义数据格式的流式传输
- 长时间运行的计算结果的流式输出
- 不需要事件格式化的简单流式数据
三、StreamingResponseBody
3.1 技术简介
StreamingResponseBody
是Spring框架提供的另一种流式响应机制,它直接操作底层输出流,提供了最大的灵活性和控制力。
3.2 实现方式
后端代码
@GetMapping("/T3")
StreamingResponseBody T3() {
return new StreamingResponseBody() {
@Override
public void writeTo(OutputStream out) throws IOException {
for (int i = 0; i < 20; i++) {
out.write(("str" + i).getBytes());
out.flush();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
// Lambda表达式的替代写法
// return outputStream -> {
// for (int i = 0; i < 20; i++) {
// outputStream.write(("str" + i).getBytes());
// outputStream.flush();
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
// };
}
技术要点:
- 直接操作原始的
OutputStream
- 提供最细粒度的输出控制
- 需要手动处理字符编码
- 需要手动调用
flush()
确保数据及时发送 - 执行会在单独的线程中进行,不需要额外创建线程
3.3 适用场景
StreamingResponseBody
特别适合:
- 大文件下载
- 需要精确控制字节输出的场景
- 视频或音频流
- 二进制数据传输
- 性能要求极高的应用
3.4 ResponseBodyEmitter 和 StreamingResponseBody 的前端接收代码
this.text = ''
const that = this
fetch('/stream/T2') // 或 '/stream/T3'
.then(res => {
const reader = res.body.getReader()
reader.read().then(function processText({ done, value }) {
if (done) return
// 处理每个数据块的逻辑
const chunk = new TextDecoder('utf-8').decode(value)
that.text += chunk
console.log(chunk)
// 读取下一个数据块
return reader.read().then(processText)
})
})
.catch(console.error)
技术要点:
- 使用Fetch API的
Response.body.getReader()
获取数据流 TextDecoder
用于将二进制数据转换为文本- 递归调用
read()
方法处理整个流 - 适用于任何响应类型,不仅限于文本
四、WebSocket 通信
4.1 技术简介
WebSocket 是一种在单个TCP连接上进行全双工通信的协议,允许服务器和客户端之间持续、双向的数据交换。与前三种技术不同,WebSocket:
- 基于TCP协议,而非HTTP
- 支持双向通信
- 具有更低的延迟
- 需要特殊的握手过程建立连接
- 支持二进制和文本传输
4.2 实现方式
后端代码 (使用Java WebSocket API)
@ServerEndpoint("/webSocket")
@Component
public class EchoServer {
// 存储所有连接的会话
private static final Set<Session> sessions = Collections.synchronizedSet(new HashSet<Session>());
@OnOpen
public void onOpen(Session session) {
System.out.println("连接成功:" + session.getId());
sessions.add(session);
}
@OnMessage
public void onMessage(Session session, String message) throws IOException {
System.out.println("收到消息:" + message);
session.getBasicRemote().sendText("服务端接收到的消息是:" + message);
// 群发消息的示例
// for (Session s : sessions) {
// s.getBasicRemote().sendText("广播消息:" + message);
// }
}
@OnClose
public void onClose(Session session) {
System.out.println("连接关闭:" + session.getId());
sessions.remove(session);
}
@OnError
public void onError(Session session, Throwable throwable) {
System.out.println("发生错误:" + session.getId());
throwable.printStackTrace();
sessions.remove(session);
}
}
技术要点:
@ServerEndpoint
注解定义WebSocket端点- 四个主要生命周期方法:
onOpen
、onMessage
、onClose
和onError
- 使用
Session
对象管理连接 - 支持一对一通信和广播
- 线程安全的会话管理
前端代码
initWebSocket() {
console.log(this.ws === null)
this.ws = new WebSocket('ws://localhost:8082/webSocket')
this.ws.onopen = () => {
this.$message.success('连接成功')
// 可以在连接成功后立即发送消息
// this.ws.send('Hello Server!')
}
this.ws.onmessage = e => {
this.receiveMsg = e.data
console.log('服务器返回消息', e.data)
}
this.ws.onclose = () => {
this.$message.error('连接关闭')
}
this.ws.onerror = () => {
this.$message.error('连接失败')
}
// 发送消息示例
// sendMessage() {
// this.ws.send(this.message)
// }
// 关闭连接
// closeConnection() {
// this.ws.close()
// }
},
技术要点:
- WebSocket API是浏览器原生支持的
- 提供事件处理机制:
onopen
、onmessage
、onclose
和onerror
- 使用
send()
方法发送消息 - 可以发送文本或二进制数据
- 需要手动关闭连接
4.3 适用场景
WebSocket特别适合:
- 实时聊天应用
- 多人在线游戏
- 协作编辑工具
- 实时监控和数据可视化
- 需要低延迟双向通信的任何应用
五、技术对比与选择指南
技术 | 通信方向 | 协议 | 自动重连 | 复杂度 | 浏览器兼容性 | 适用场景 |
---|---|---|---|---|---|---|
SSE/SseEmitter | 服务器→客户端 | HTTP | 是 | 低 | 良好 | 实时通知、流式更新 |
ResponseBodyEmitter | 服务器→客户端 | HTTP | 否 | 中 | 优秀 | 通用数据流 |
StreamingResponseBody | 服务器→客户端 | HTTP | 否 | 中 | 优秀 | 大文件、精确控制 |
WebSocket | 双向 | WebSocket | 否(需自行实现) | 高 | 优秀 | 实时交互、聊天应用 |
选择建议
-
只需服务器到客户端的单向通信:
- 对于简单场景,选择 SseEmitter (SSE)
- 对于需要更多控制的场景,选择 ResponseBodyEmitter
- 对于二进制数据或大文件,选择 StreamingResponseBody
-
需要客户端到服务器的通信:
- WebSocket 是唯一选择
-
考虑因素:
- 性能需求
- 客户端兼容性
- 开发复杂度
- 网络环境(如防火墙可能阻止WebSocket)
- 数据格式需求
六、实际应用示例
6.1 AI聊天机器人的流式响应实现
使用SSE实现类似ChatGPT的流式文本响应:
@GetMapping("/chat/stream")
public SseEmitter chatStream(@RequestParam String prompt) {
SseEmitter emitter = new SseEmitter(-1L); // 无超时
aiService.processPromptAsync(prompt,
(chunk) -> {
try {
emitter.send(SseEmitter.event().data(chunk));
} catch (IOException e) {
emitter.completeWithError(e);
}
},
() -> emitter.complete()
);
return emitter;
}
6.2 实时数据大屏幕
使用WebSocket实现实时更新的数据可视化:
@Scheduled(fixedRate = 5000)
public void broadcastMetrics() {
MetricsData data = metricsService.getCurrentMetrics();
for (Session session : sessions) {
try {
session.getBasicRemote().sendText(objectMapper.writeValueAsString(data));
} catch (IOException e) {
// 处理错误
}
}
}
6.3 大文件下载
使用StreamingResponseBody实现大文件下载:
@GetMapping("/download/{fileId}")
public StreamingResponseBody downloadFile(@PathVariable String fileId) {
File file = fileService.getFile(fileId);
return outputStream -> {
try (FileInputStream inputStream = new FileInputStream(file)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
outputStream.flush();
}
}
};
}
七、性能优化与注意事项
7.1 通用优化建议
-
配置适当的超时时间
- 例如:
new SseEmitter(300000L); // 5分钟超时
- 例如:
-
使用线程池而非单个线程
@Autowired private TaskExecutor taskExecutor; @GetMapping("/stream") public SseEmitter stream() { SseEmitter emitter = new SseEmitter(); taskExecutor.execute(() -> { // 异步处理逻辑 }); return emitter; }
-
错误处理与资源清理
- 总是处理异常并关闭连接
- 使用try-with-resources确保资源释放
-
限制并发连接数
- 使用信号量或限流器防止服务器过载
7.2 常见问题与解决方案
-
代理服务器问题
- Nginx等代理可能缓冲响应,需配置:
proxy_buffering off; proxy_read_timeout 300s;
- Nginx等代理可能缓冲响应,需配置:
-
WebSocket连接断开
- 实现心跳机制保持连接活跃
- 客户端断线重连逻辑
-
内存泄漏
- 确保清理未使用的Emitter对象
- 对长连接实现超时管理
-
压力测试
- 在部署前测试系统在高并发下的表现
- 确定每种技术的性能边界
八、结论
前后端流式传输技术极大地改善了Web应用的用户体验。选择合适的技术取决于具体的应用需求、性能要求和开发复杂度。
- SSE/SseEmitter: 简单易用的单向服务器推送
- ResponseBodyEmitter: 灵活的HTTP流式响应
- StreamingResponseBody: 精细控制的二进制流传输
- WebSocket: 功能强大的双向实时通信
在实际项目中,可能需要结合使用多种技术来满足不同的业务场景需求。
项目实战示例:实时通知练习项目