跳到主要内容Spring Boot 集成 WebSocket 实战:实现后台向前端实时推送 | 极客日志Java大前端java
Spring Boot 集成 WebSocket 实战:实现后台向前端实时推送
WebSocket 协议突破 HTTP 请求响应限制,实现服务端主动推送。通过 Spring Boot 集成 WebSocket,可构建高实时性应用。内容涵盖原生接口与 STOMP 消息代理两种实现路径,深入探讨安全鉴权、集群会话共享及性能调优,辅以完整前后端代码示例与实战通知系统案例。
清酒独酌2 浏览 1. 引言
随着互联网应用对实时性要求的提升,传统 HTTP 协议基于请求 - 响应模式的局限性日益凸显。客户端发起请求,服务器返回响应后连接关闭,这种'拉取'模式在处理股票行情、即时消息或系统通知时显得力不从心:频繁轮询浪费资源,而服务器有新数据时又无法主动通知。
WebSocket 协议的出现完美解决了这一痛点。它允许服务器主动向客户端推送数据,实现真正的全双工通信。Spring Boot 作为主流的 Java 微服务框架,对 WebSocket 提供了良好的支持。本文将深入讲解如何在 Spring Boot 中集成 WebSocket,涵盖原生实现、STOMP 协议、安全集成及集群部署等核心场景,帮助读者构建高实时性的后端推送能力。
2. WebSocket 基础
2.1 什么是 WebSocket?
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,由 IETF 在 2011 年定为标准 RFC 6455。它简化了客户端与服务器的数据交换,允许服务端主动推送数据,无需客户端反复轮询。
2.2 WebSocket 与 HTTP 的关系
两者相辅相成。WebSocket 建立连接时使用 HTTP 协议的 Upgrade 机制进行协议升级:客户端携带特殊头部(Connection: Upgrade 和 Upgrade: websocket)发起请求,服务器返回 101 状态码后,连接从 HTTP 切换至 WebSocket,后续通信不再受限于 HTTP 格式。
- 相同点:均基于 TCP,默认端口为 80 (ws) 和 443 (wss)。
- 不同点:HTTP 是半双工且需频繁建立连接,WebSocket 是全双工且连接持久化,消息头开销更小。
2.3 工作流程
- 握手阶段:客户端发起 HTTP 请求,携带
Upgrade: websocket 头。
- 协议切换:服务器返回 101 状态码,同意切换。
- 数据传输:双方互相发送文本或二进制数据帧。
- 关闭连接:任意一方发送关闭帧,另一方响应后断开 TCP。
3. Spring Boot 对 WebSocket 的支持
Spring 框架从 4.0 开始引入 WebSocket 模块,Spring Boot 通过自动配置进一步简化了集成。主要支持两种方式:
- 原生 WebSocket:使用
@ServerEndpoint 注解,基于 JSR-356 实现。
- STOMP over WebSocket:在 WebSocket 之上使用 STOMP 协议,提供类似消息队列的订阅发布模型,支持更高级的路由功能。
4. 项目准备
创建一个基本的 Spring Boot 项目,选择以下依赖:
- Spring Web
- WebSocket (
spring-boot-starter-websocket)
Maven 核心依赖如下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket
org.springframework.boot
spring-boot-starter-web
com.fasterxml.jackson.core
jackson-databind
</artifactId>
</dependency>
<dependency>
<groupId>
</groupId>
<artifactId>
</artifactId>
</dependency>
<dependency>
<groupId>
</groupId>
<artifactId>
</artifactId>
</dependency>
</dependencies>
5. 基于原生 WebSocket 的实现
我们先从最基础的原生 WebSocket 入手,理解握手与消息收发流程。
5.1 配置处理器
在 Spring Boot 中使用原生 WebSocket,通常通过 @ServerEndpoint 注解标记类。需要注册一个 ServerEndpointExporter Bean 来自动扫描并注册端点。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
注意:如果使用内嵌 Servlet 容器(如 Tomcat),ServerEndpointExporter 会自动注册端点。若部署到外部容器,可能需要额外配置。
5.2 编写处理类
使用 @OnOpen、@OnMessage 等注解处理事件。下面是一个简单的回声示例,同时支持广播推送:
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
@ServerEndpoint("/ws/echo")
public class EchoWebSocket {
private static final CopyOnWriteArraySet<Session> sessions = new CopyOnWriteArraySet<>();
@OnOpen
public void onOpen(Session session) {
sessions.add(session);
System.out.println("新连接加入,当前连接数:" + sessions.size());
try {
session.getBasicRemote().sendText("连接成功,欢迎!");
} catch (IOException e) {
e.printStackTrace();
}
}
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("收到消息:" + message);
try {
session.getBasicRemote().sendText("Echo: " + message);
} catch (IOException e) {
e.printStackTrace();
}
broadcast("用户说:" + message);
}
@OnClose
public void onClose(Session session) {
sessions.remove(session);
System.out.println("连接关闭,当前连接数:" + sessions.size());
}
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
public static void broadcast(String message) {
for (Session session : sessions) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
5.3 前端测试
前端使用浏览器原生 WebSocket API 连接后端:
<!DOCTYPE html>
<html>
<head><title>WebSocket Echo Test</title></head>
<body>
<input type="text" id="messageInput" placeholder="输入消息" />
<button onclick="sendMessage()">发送</button>
<div id="messages"></div>
<script>
var ws = new WebSocket("ws://localhost:8080/ws/echo");
ws.onopen = function() { appendMessage("连接已建立"); };
ws.onmessage = function(event) {
appendMessage("收到:" + event.data);
};
function sendMessage() {
var input = document.getElementById("messageInput");
var msg = input.value;
ws.send(msg);
appendMessage("发送:" + msg);
input.value = '';
}
function appendMessage(msg) {
var div = document.getElementById("messages");
div.innerHTML += "<p>" + msg + "</p>";
}
</script>
</body>
</html>
启动应用访问该页面即可测试双向通信。如需后台主动推送,可在 Controller 或定时任务中调用 broadcast() 方法。
5.4 优缺点分析
- 优点:简单直接,依赖少,适合小规模应用。
- 缺点:需自行管理会话和线程安全;缺乏高级消息路由;WebSocket 实例非 Spring 管理,难以直接使用依赖注入。
6. 基于 STOMP 的实现
STOMP(Simple Text Oriented Messaging Protocol)定义了一套基于帧的格式,可以在 WebSocket 之上使用。Spring 提供的 STOMP over WebSocket 支持让我们像使用消息队列一样处理消息,极大简化了开发。
6.1 配置消息代理
启用 STOMP 需要创建配置类,实现 WebSocketMessageBrokerConfigurer 接口。
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-stomp")
.setAllowedOrigins("*")
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker("/topic", "/queue");
registry.setUserDestinationPrefix("/user");
}
}
registerStompEndpoints:注册连接端点。withSockJS() 表示如果浏览器不支持 WebSocket,可降级使用其他传输方式。
configureMessageBroker:配置消息路由。/app 是客户端发给服务器的路径前缀,/topic 和 /queue 是服务器分发消息的路径前缀。
6.2 消息控制器
使用 @MessageMapping 注解处理客户端发送到特定目的地的消息。
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
@Controller
public class WebSocketController {
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public Greeting greeting(HelloMessage message) throws Exception {
Thread.sleep(1000);
return new Greeting("Hello, " + message.getName() + "!");
}
}
class HelloMessage {
private String name;
}
class Greeting {
private String content;
}
@MessageMapping("/hello"):当客户端发送到 /app/hello 的消息会路由到此方法。
@SendTo("/topic/greetings"):返回值将发送到 /topic/greetings 目的地,所有订阅该主题的客户端都会收到。
6.3 使用 SimpMessagingTemplate 推送
SimpMessagingTemplate 是 Spring 提供的推送工具类,可注入到任何 Bean 中灵活使用。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PushController {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@GetMapping("/push")
public String pushToAll() {
messagingTemplate.convertAndSend("/topic/news", "突发新闻:Spring Boot 3.0 发布!");
return "推送成功";
}
@GetMapping("/push/user")
public String pushToUser(String userId) {
messagingTemplate.convertAndSendToUser(userId, "/message", "您有一条私信");
return "私信推送成功";
}
}
convertAndSend 用于广播,convertAndSendToUser 用于点对点。注意 convertAndSendToUser 默认会拼接成 /user/{userId}/message 这样的目的地。
6.4 前端集成
前端需引入 SockJS 和 STOMP.js 库。
<!DOCTYPE html>
<html>
<head>
<title>STOMP over WebSocket Demo</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
</head>
<body>
<div>
<input type="text" id="name" placeholder="你的名字" />
<button onclick="sendName()">发送</button>
</div>
<div id="greetings"></div>
<script>
var stompClient = null;
function connect() {
var socket = new SockJS('/ws-stomp');
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
console.log('连接成功:' + frame);
stompClient.subscribe('/topic/greetings', function(greeting) {
showGreeting(JSON.parse(greeting.body).content);
});
stompClient.subscribe('/user/message', function(message) {
showGreeting("私信:" + message.body);
});
}, function(error) {
console.log('连接失败:' + error);
});
}
function sendName() {
var name = document.getElementById('name').value;
stompClient.send("/app/hello", {}, JSON.stringify({ 'name': name }));
}
function showGreeting(message) {
var div = document.getElementById('greetings');
div.innerHTML += "<p>" + message + "</p>";
}
window.onload = connect;
</script>
</body>
</html>
7. 安全集成
实际应用中,WebSocket 连接往往需要鉴权。Spring Security 提供了与 WebSocket 的集成。
7.1 配置 Security
添加 Spring Security 依赖,并配置允许 WebSocket 端点访问。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/ws-stomp/**").authenticated()
.anyRequest().permitAll()
.and()
.formLogin()
.permitAll()
.and()
.logout()
.permitAll()
.and()
.csrf().disable();
return http.build();
}
}
7.2 获取用户信息
为了让 WebSocket 知道当前用户是谁,我们需要在握手阶段传递认证信息。一种常见做法是结合 Spring Security,在连接时通过 Cookie 或 Header 传递 token。
在 STOMP 配置中添加 ChannelInterceptor,用于在消息进入时设置用户上下文。
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
public class UserInterceptor implements ChannelInterceptor {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (accessor != null && accessor.getCommand() != null) {
switch (accessor.getCommand()) {
case CONNECT:
String token = accessor.getFirstNativeHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
}
break;
}
}
return message;
}
}
如果已通过 HTTP 登录(如表单登录),由于同源策略,JSESSIONID Cookie 会自动携带,Spring Security 会识别出已登录用户,自动绑定到 WebSocket 会话上。
8. 集群环境下的 WebSocket
在生产环境中,应用通常部署多个实例。WebSocket 连接是状态化的,每个连接对应一个会话,保存在节点内存中。在集群环境下,若用户连接到实例 A,但推送消息时由实例 B 发送,会导致推送失败。
8.1 解决方案
- 使用外部消息代理:如 RabbitMQ、Kafka。所有节点连接到同一个代理,消息通过代理分发。
- 会话集中存储:将会话信息存储在 Redis 中,推送时查找用户所在节点再转发。此法实现较复杂。
8.2 配置 RabbitMQ
rabbitmq-plugins enable rabbitmq_stomp
修改 Spring Boot 配置,使用 StompBrokerRelay 代替简单内存代理:
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableStompBrokerRelay("/topic", "/queue")
.setRelayHost("localhost")
.setRelayPort(61613)
.setClientLogin("guest")
.setClientPasscode("guest")
.setSystemLogin("guest")
.setSystemPasscode("guest");
registry.setUserDestinationPrefix("/user");
}
这样配置后,消息的广播和点对点将通过 RabbitMQ 进行分发,实现跨节点通信。
9. 性能优化与最佳实践
9.1 心跳机制
WebSocket 本身有 Ping/Pong 帧,STOMP 也支持心跳协商。在 Spring 中可配置心跳间隔:
registry.enableSimpleBroker("/topic", "/queue")
.setHeartbeatValue(new long[]{10000, 10000});
9.2 线程模型
Spring WebSocket 使用任务线程池处理消息。默认情况下会配置 ThreadPoolTaskExecutor。高并发下建议自定义线程池参数,防止线程耗尽。
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.taskExecutor()
.corePoolSize(10)
.maxPoolSize(20)
.keepAliveSeconds(60);
}
9.3 消息大小限制
可通过 WebSocketTransportProperties 调整,或配置 StompBrokerRelay 的 messageSizeLimit。
@Bean
public AbstractBrokerMessageHandler stompBrokerRelayMessageHandler(
StompBrokerRelayRegistration registration, MessageBrokerProperties properties) {
StompBrokerRelayMessageHandler handler = new StompBrokerRelayMessageHandler(registration);
handler.setMessageSizeLimit(1024 * 1024);
return handler;
}
10. 实战案例:实时通知系统
10.1 需求描述
- 系统广播:所有在线用户都能收到(如维护公告)。
- 个人私信:仅特定用户收到(如评论回复)。
10.2 后端实现
public class Notification {
private String type;
private String content;
private String targetUser;
private Date timestamp;
}
@MessageMapping("/private")
public void sendPrivate(@Payload Notification notification, Principal principal) {
notification.setTimestamp(new Date());
messagingTemplate.convertAndSendToUser(notification.getTargetUser(), "/queue/notifications", notification);
}
@RestController
@RequestMapping("/api/notify")
public class NotifyController {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@PostMapping("/broadcast")
public String broadcast(@RequestBody String content) {
Notification notification = new Notification("BROADCAST", content, null, new Date());
messagingTemplate.convertAndSend("/topic/broadcast", notification);
return "广播成功";
}
}
10.3 前端监听
function connect() {
var socket = new SockJS('/ws-stomp');
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
stompClient.subscribe('/topic/broadcast', function(msg) {
var notif = JSON.parse(msg.body);
showNotification(notif.content);
});
stompClient.subscribe('/user/queue/notifications', function(msg) {
var notif = JSON.parse(msg.body);
showNotification("私信:" + notif.content);
});
});
}
启动应用,用不同用户登录测试,即可验证广播与私信功能。
11. 常见问题排查
- 连接失败(404):检查端点路径是否正确,是否启用了 SockJS,以及跨域配置。
- 消息丢失:确认客户端订阅了正确的目的地,点对点消息确保用户名正确。
- 断线重连:前端监听断开事件并延时重连,服务端设置心跳检测失效连接。
- 跨域问题:在
registerStompEndpoints 中通过 setAllowedOrigins 允许域名,生产环境应指定具体域名。
12. 总结
本文从 WebSocket 的基本概念出发,详细介绍了在 Spring Boot 中集成 WebSocket 的两种方式:原生 WebSocket 和基于 STOMP 的消息代理方式。我们探讨了消息的广播与点对点推送、安全集成、集群部署以及性能优化。通过实战案例,展示了如何构建一个实时通知系统。WebSocket 技术为实时 Web 应用提供了强大的支持,结合 Spring Boot 的便捷性,开发者可以快速构建出高实时性的应用。
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online