Spring MVC文件上传SSE与WebSocket详解

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------复杂度与收益要匹配。


五、总结

  1. 文件上传:MultipartFile 接收 + 大小配置;生产用对象存储/直传,大文件分片;校验类型、防路径穿越。
  2. SSE:HTTP 长连接单向推送,SseEmitter 异步发送,浏览器 EventSource 自动重连;简单可靠。
  3. WebSocket:握手升级全双工;原生 @EnableWebSocket 或 STOMP 订阅广播;注意心跳、会话管理、集群广播。
  4. 选型:轮询 < 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 增加运维成本(会话、集群广播、心跳)。

相关推荐
一个有温度的技术博主19 小时前
两级缓存一致性实战:Redis + 本地缓存,如何不让数据“分裂”?
redis·spring·缓存
步行cgn19 小时前
Spring AOP 实战:安全日志
python·安全·spring
做个文艺程序员19 小时前
MQ第04篇:ActiveMQ深度解析:JMS规范、Spring JMS整合与企业级异步通信实战
spring·消息队列·activemq·java-activemq·jms
Rain的Java大神之路20 小时前
[特殊字符]8年Java老兵转型AI Agent:90%的人挂在同一个坑,根本不用学Python!(万字实战复盘,建议收藏)
java·数据库·人工智能·后端·python·spring·ai编程
桃西西呀21 小时前
Spring AI Alibaba 之九:我让 Agent 接着上一轮往下聊,它却只还给我一条消息,多轮记忆到底藏在哪
人工智能·spring·llm
开开心心_Every1 天前
内存清理工具定时自动清理开机自启动
java·开发语言·spring·计算机外设·ocr·intellij-idea·excel
我是人✓1 天前
Spring框架的事务管理
java·后端·spring