Spring MVC文件上传SSE与WebSocket详解
定位:第 05 篇,讲透文件上传、SSE 服务端推送、WebSocket 全双工通信的原理与 Spring 用法,以及三者选型
适用版本:Spring Framework 6.x(JDK 17+)
目录
一、文件上传
1.1 基本用法
表单 enctype="multipart/form-data" 提交,控制器用 MultipartFile 接收:
java
@PostMapping("/upload")
String upload(@RequestParam("file") MultipartFile file) throws IOException {
file.getOriginalFilename();
file.transferTo(new File(destDir, file.getOriginalFilename()));
return "ok";
}
1.2 配置与限制
# BT-05 已述
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=20MB
同时注意反向代理(Nginx client_max_body_size)与内嵌容器层的限制,任一层卡住都失败。
1.3 存储与大文件
| 策略 | 说明 |
|---|---|
| 对象存储(推荐) | 上传到 OSS/S3,应用不落盘,天然分布式 |
| 本地磁盘 | 单机/小规模,注意多实例共享问题 |
| 直传 | 客户端直接传对象存储(临时凭证),不经应用转发 |
| 分片上传 | 大文件切片并行传,服务端合并,支持断点续传 |
大文件经应用转发会占带宽与内存,生产优先对象存储直传。
1.4 安全
- 校验文件类型(不只看扩展名,查 MIME/魔数);
- 重命名存储,防路径穿越(
../); - 限制大小与频率,防资源耗尽。
二、SSE(服务端推送)
2.1 原理
基于 HTTP 的服务端单向推送 :客户端发起一个长连接请求,服务端保持连接持续推送事件(Content-Type: text/event-stream),浏览器 EventSource 原生支持、自动重连。
2.2 Spring 用法(SseEmitter)
java
@GetMapping("/stream")
SseEmitter stream() {
SseEmitter emitter = new SseEmitter(60_000L); // 超时 60s
executor.execute(() -> {
try {
emitter.send(SseEmitter.event().name("msg").data("hello"));
emitter.complete(); // 结束
} catch (IOException e) {
emitter.completeWithError(e);
}
});
return emitter; // 立即返回,连接保持
}
要点:在异步线程 里持续 send;complete() 关闭;注册 onCompletion/onTimeout/onError 回调清理资源。
2.3 适用场景
通知推送、任务进度、实时数据展示(股票/日志)。只要服务端→客户端单向,SSE 比 WebSocket 简单得多。
三、WebSocket
3.1 协议
通过 HTTP 握手升级(Upgrade: websocket),建立全双工 长连接(ws:///wss://),之后双方可随时互发消息。
3.2 Spring 两种支持
原生 WebSocket:
java
@EnableWebSocket
class WsConfig implements WebSocketConfigurer {
public void registerWebSocketHandlers(WebSocketHandlerRegistry reg) {
reg.addHandler(new MyHandler(), "/ws").setAllowedOrigins("*");
}
}
class MyHandler extends TextWebSocketHandler {
protected void handleTextMessage(WebSocketSession s, TextMessage m) {
s.sendMessage(new TextMessage("echo: " + m.getPayload()));
}
}
STOMP(子协议,消息代理模式):
@EnableWebSocketMessageBroker
客户端 SUBSCRIBE /topic/news → 服务端 convertAndSend("/topic/news", data)
@MessageMapping("/chat") 接收,@SendTo 广播
| 方式 | 适用 |
|---|---|
| 原生 @EnableWebSocket | 简单自定义协议 |
| STOMP | 订阅/广播/点对点,复杂消息场景 |
3.3 工程要点
- 心跳:检测死连接(STOMP 内置心跳);
- 会话管理:维护 userId → session 映射,连接关闭时清理;
- 集群广播:多实例需消息总线(如 STOMP broker relay / Redis)转发到持有目标连接的实例;
- 鉴权:握手阶段或首帧校验。
四、三者选型
| 方式 | 方向 | 复杂度 | 适用 |
|---|---|---|---|
| 轮询 | 客户端反复问 | 低 | 低频、简单,但浪费请求 |
| SSE | 服务端 → 客户端 | 中 | 单向推送(通知/进度/实时数据),浏览器原生 |
| WebSocket | 双向 | 高 | 聊天、协作、双向实时 |
决策:能用轮询就轮询,单向推送用 SSE,双向实时才上 WebSocket------复杂度与收益要匹配。
五、总结
- 文件上传:MultipartFile 接收 + 大小配置;生产用对象存储/直传,大文件分片;校验类型、防路径穿越。
- SSE:HTTP 长连接单向推送,SseEmitter 异步发送,浏览器 EventSource 自动重连;简单可靠。
- WebSocket:握手升级全双工;原生 @EnableWebSocket 或 STOMP 订阅广播;注意心跳、会话管理、集群广播。
- 选型:轮询 < SSE < WebSocket 复杂度递增,按方向与实时性匹配。
六、常见高频面试题
1. Spring 如何处理文件上传?
要点:客户端以 multipart/form-data 提交;服务端配置 multipart 解析(Boot 自动),控制器方法参数用 MultipartFile 接收,可获取文件名/类型/输入流,transferTo 保存或转存对象存储。需配置大小限制(max-file-size/max-request-size),并注意反向代理层的限制。生产推荐对象存储,大文件分片,校验类型与安全。
2. 什么是 SSE?和 WebSocket 怎么选?
要点:SSE(Server-Sent Events)是基于 HTTP 的服务端单向推送------客户端发起长连接,服务端持续推送事件,浏览器 EventSource 原生支持且自动重连。选择:只需服务端→客户端单向(通知、进度、实时数据)用 SSE,实现简单、走 HTTP 基础设施友好;需要双向实时(聊天、协作)才用 WebSocket。SSE 天然过代理/防火墙更顺畅。
3. WebSocket 的连接是如何建立的?
要点:先通过一次 HTTP 握手------客户端发带 Upgrade: websocket、Connection: Upgrade 头的请求,服务端响应 101 Switching Protocols,之后该 TCP 连接升级为 WebSocket 协议(ws://),双方可全双工随时互发消息。它复用 80/443 端口,能穿越多数防火墙。关闭时一方发关闭帧,连接终止。
4. STOMP 是什么?什么时候用?
要点:STOMP 是运行在 WebSocket 之上的消息子协议,提供订阅/发布语义(类似轻量 MQ)。Spring 用 @EnableWebSocketMessageBroker 启用:客户端 SUBSCRIBE 目的地(如 /topic/news),服务端 convertAndSend 广播,@MessageMapping 处理消息。适合多订阅、广播、点对点消息的复杂场景,免去自建路由;简单场景用原生 WebSocket 即可。
5. WebSocket 在多实例部署下如何广播消息?
要点:用户连接分散在不同实例上,发消息的实例未必持有目标连接。方案:① 引入消息总线(Redis/专用 broker),实例把消息发到总线,持有目标连接的实例订阅并下发;② STOMP 用外部 broker(broker relay)。同时用 Redis 等记录"用户在哪个实例"用于定向路由。核心是"连接状态在单实例,消息靠总线中转"。
6. SseEmitter 要注意哪些问题?
要点:① 在异步线程中发送(send),不阻塞请求线程;② 设置超时并注册 onTimeout/onCompletion/onError 回调,及时清理,防连接泄漏;③ 客户端断开(浏览器关闭)要感知并移除,避免向死连接发送;④ 注意中间代理的缓冲与超时(可能切断长连接);⑤ 完成后调 complete()。SSE 连接是有状态资源,必须管理生命周期。
7. 文件上传如何保证安全?
要点:① 校验真实类型------不只看扩展名,检查 MIME 与文件头(魔数),防可执行文件伪装;② 重命名存储,避免原名带来的路径穿越(.../)与覆盖;③ 限制大小与上传频率,防资源耗尽攻击;④ 存储位置不可被直接执行/访问(对象存储私有 + 签名访问);⑤ 病毒/内容扫描视业务需要。
8. 什么是长轮询?和 SSE 的区别?
要点:长轮询是客户端发请求后服务端挂起不立即返回,直到有数据或超时才响应,客户端收到后立即再发------模拟实时但仍是"请求-响应"循环。区别:SSE 是单条持久连接持续推送,无需反复建立请求,浏览器自动重连,更省资源、语义更清晰。短轮询浪费、长轮询占连接,SSE/WebSocket 是更现代的服务端推送方案。
9. WebSocket 如何做心跳保活?
要点:长连接可能被中间设备静默回收,或对端已死但 TCP 未感知,需心跳探测。STOMP 内置心跳(双方按约定间隔发心跳帧,超时判定断开);原生 WebSocket 可定时发 Ping 帧(或应用层心跳消息),对端回 Pong,长时间无响应则判定死连接并关闭清理。心跳间隔需小于中间设备的空闲超时。
10. 实时推送功能,从轮询、SSE、WebSocket 中你会怎么选?
要点:按方向与实时性递进选择。数据更新低频(秒级以上)且客户端主动即可------轮询最简单;需要服务端及时单向推送(通知、进度、行情)------SSE,实现简单、浏览器原生重连、过基础设施友好;需要双向实时交互(聊天、协同编辑、游戏)------WebSocket。原则是"够用即可",复杂度要与收益匹配,避免一上来就上 WebSocket 增加运维成本(会话、集群广播、心跳)。
