跳到主要内容 Spring Boot集成WebSocket,实现后台向前端推送信息 | 极客日志
Python
Spring Boot集成WebSocket,实现后台向前端推送信息 1\. 引言 随着互联网应用的不断发展,用户对实时性的要求越来越高。传统的HTTP协议是基于请求-响应模式的,客户端发起请求,服务器返回响应,连接即关闭。这种'拉取'模式在处理实时数据(如股票行情、即时消息、游戏对战、系统通知等)时显得力不从心:要么客户端频繁轮询造成资源浪费,要么服务器有新数据却无法主动通知客户端。 WebSocket协议的出现完美解决了这一难题。它允许服务器主动向客户端推送数据…
指针猎手 发布于 2026/4/6 更新于 2026/4/18 20K 浏览1. 引言
随着互联网应用的不断发展,用户对实时性的要求越来越高。传统的HTTP协议是基于请求-响应模式的,客户端发起请求,服务器返回响应,连接即关闭。这种'拉取'模式在处理实时数据(如股票行情、即时消息、游戏对战、系统通知等)时显得力不从心:要么客户端频繁轮询造成资源浪费,要么服务器有新数据却无法主动通知客户端。
WebSocket协议的出现完美解决了这一难题。它允许服务器主动向客户端推送数据,实现真正的双向通信。Spring Boot作为当今最流行的Java微服务框架,对WebSocket提供了良好的支持。本文将深入浅出地讲解如何在Spring Boot中集成WebSocket,实现后台向前端推送信息,涵盖原生WebSocket、STOMP协议、安全集成、集群部署等方方面面,力求让读者能够全面掌握这一技术。
2. WebSocket基础
2.1 什么是WebSocket?
WebSocket Echo Test
发送
STOMP over WebSocket Demo
WebSocket是一种在单个TCP连接上进行全双工通信的协议。它由IETF在2011年定为标准RFC 6455,并被Web API定义为W3C标准。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。
2.2 WebSocket与HTTP的关系 WebSocket与HTTP协议是相辅相成的。WebSocket在建立连接时使用HTTP协议的Upgrade机制进行协议升级:客户端发起一个带有特殊头部(Connection: Upgrade和Upgrade: websocket)的HTTP请求,服务器如果支持WebSocket,则返回101状态码(Switching Protocols),之后连接便从HTTP协议切换到WebSocket协议,后续通信不再使用HTTP格式。
相同点 :
都基于TCP协议。
默认端口也是80和443(ws对应80,wss对应443)。
不同点 :
HTTP是半双工,WebSocket是全双工。
HTTP需要频繁建立连接(尤其是HTTP/1.1虽支持keep-alive,但仍是请求-响应模式),WebSocket连接一旦建立,可以持续通信。
WebSocket消息没有HTTP那种复杂的头部,开销小。
2.3 WebSocket的工作流程
握手阶段 :客户端发起HTTP请求,携带Upgrade: websocket头,请求升级协议。
协议切换 :服务器返回101状态码,同意切换,连接协议变为WebSocket。
数据传输 :客户端和服务器可以互相发送数据帧(数据单元),可以是文本或二进制。
关闭连接 :任意一方可以发送关闭帧,另一方响应后关闭TCP连接。
2.4 WebSocket的优点
实时性 :服务器可以随时推送消息,延迟极低。
减少网络开销 :相比轮询,减少了大量的HTTP头部传输。
全双工 :双方可以同时发送数据,更自然。
跨域支持 :可通过CORS或特殊配置支持跨域。
3. Spring Boot对WebSocket的支持 Spring框架从4.0开始引入了WebSocket模块,提供了一套简洁的API来集成WebSocket。Spring Boot则通过自动配置进一步简化了集成过程。主要支持两种方式:
原生WebSocket :使用@ServerEndpoint注解,基于Java WebSocket API(JSR-356)实现。
STOMP over WebSocket :在WebSocket之上使用STOMP协议(简单文本定向消息协议),提供更高级的消息路由功能,类似于消息队列的订阅发布模型。
Spring还提供了SimpMessagingTemplate用于向客户端推送消息,以及@MessageMapping注解处理客户端发送的消息。
4. 准备工作:创建Spring Boot项目
Spring Web
Spring Boot DevTools(可选)
WebSocket (即spring-boot-starter-websocket)
如果你使用Maven,pom.xml中的核心依赖如下:
org.springframework.boot spring-boot-starter-websocket org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-devtools runtime true com.fasterxml.jackson.core jackson-databind
implementation 'org.springframework.boot:spring-boot-starter-websocket' implementation 'org.springframework.boot:spring-boot-starter-web' runtimeOnly 'org.springframework.boot:spring-boot-devtools'
5. 基于原生WebSocket的实现 我们先从最基础的原生WebSocket开始,了解握手、消息收发等基本流程。
5.1 配置WebSocket处理器 在Spring Boot中使用原生WebSocket,通常有两种方式:一种是使用@ServerEndpoint注解,另一种是实现WebSocketHandler接口。这里我们采用更符合Spring风格的@ServerEndpoint方式。
首先,需要注册一个ServerEndpointExporter Bean,它会自动扫描并注册带有@ServerEndpoint注解的类。在Spring Boot中,通常通过配置类完成:
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会自动注册端点。如果部署到外部容器,可能需要额外配置,但Spring Boot内嵌容器通常够用。
5.2 编写WebSocket处理类 使用@ServerEndpoint注解标记一个类,并定义其路径。然后通过@OnOpen、@OnMessage、@OnClose、@OnError注解来处理相应的事件。
下面是一个简单的WebSocket服务端示例,它接收客户端消息并原样返回(回声),同时支持主动推送:
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 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 前端JavaScript示例 前端使用浏览器原生的WebSocket API连接后端:
启动Spring Boot应用,访问该HTML页面,即可测试双向通信。
5.4 后台主动推送消息 我们可以在任何地方(如定时任务、Controller中)调用EchoWebSocket.broadcast()来向所有连接的客户端推送消息。例如,创建一个定时任务每秒推送当前时间:
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component @EnableScheduling public class PushTask { @Scheduled(fixedRate = 5000) public void pushTime() { String message = "当前时间:" + System.currentTimeMillis(); EchoWebSocket.broadcast(message); } }
5.5 原生方式的优缺点
优点 :简单直接,依赖少,适合小规模应用。
缺点 :需要自己管理会话、处理线程安全;缺乏高级消息路由(如广播给特定用户);与Spring的集成度不高(例如无法直接利用Spring的依赖注入,因为WebSocket实例不是Spring管理的,但可以通过静态方法或工具类解决)。
6. 基于STOMP的实现 STOMP(Simple Text Oriented Messaging Protocol)是一个简单的文本定向消息协议,它定义了一套基于帧的格式,可以在WebSocket之上使用。Spring提供了对STOMP over WebSocket的支持,使得我们可以像使用消息队列一样处理消息,支持目的地(destination)、订阅等概念,极大简化了开发。
6.1 STOMP简介 STOMP类似于HTTP,但更简单。它的帧格式如下:
COMMAND header1:value1 header2:value2 body
常用命令:SEND(发送消息)、SUBSCRIBE(订阅目的地)、UNSUBSCRIBE、MESSAGE(消息推送)、CONNECT、CONNECTED等。
在WebSocket上使用STOMP,我们可以将消息路由到不同的处理器,支持点对点和广播。
6.2 配置WebSocket消息代理 在Spring Boot中启用STOMP over WebSocket,需要创建一个配置类,实现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) { // 注册一个STOMP端点,客户端通过这个端点进行连接 registry.addEndpoint("/ws-stomp") .setAllowedOrigins("*") // 允许跨域 .withSockJS(); // 启用SockJS支持,用于降级方案 } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 设置应用目的地前缀,即客户端发送消息的目的地前缀 registry.setApplicationDestinationPrefixes("/app"); // 启用简单消息代理,并设置消息代理前缀(订阅前缀) registry.enableSimpleBroker("/topic", "/queue"); // 点对点使用的订阅前缀(默认是/user/) registry.setUserDestinationPrefix("/user"); } }
registerStompEndpoints:注册STOMP端点,客户端通过该端点连接。withSockJS()表示如果浏览器不支持WebSocket,可以降级使用SockJS(基于其他传输方式模拟)。
configureMessageBroker:配置消息代理。
setApplicationDestinationPrefixes:客户端发送消息给服务器的路径前缀(如/app/hello)。
enableSimpleBroker:启用内置的简单消息代理,并指定代理前缀(如/topic用于广播,/queue用于点对点)。
setUserDestinationPrefix:设置用户目的地前缀,用于点对点推送(如/user/{userId}/message)。
6.3 创建Controller处理消息 在Spring MVC中,我们可以使用@MessageMapping注解来处理客户端发送到特定目的地的消息。这些方法通常返回一个对象,该对象会被自动转换并发送到指定的目的地。
import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.stereotype.Controller; @Controller public class WebSocketController { /** * 客户端发送消息到 /app/hello,服务器处理后,将结果广播给所有订阅了 /topic/greetings 的客户端 */ @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; // getter/setter... } class Greeting { private String content; // constructor, getter/setter... }
@MessageMapping("/hello"):表示当客户端发送到/app/hello(因为我们在配置中设置了应用前缀/app)的消息会路由到此方法。
@SendTo("/topic/greetings"):指定方法的返回值将发送到/topic/greetings目的地,所有订阅了该目的地的客户端都会收到。
如果不使用@SendTo,也可以使用SimpMessagingTemplate手动发送。
6.4 使用SimpMessagingTemplate推送消息 SimpMessagingTemplate是Spring提供的用于向客户端推送消息的工具类。我们可以将它注入到任何Spring管理的Bean中,灵活地推送消息。
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.GetMapping; @RestController public class PushController { @Autowired private SimpMessagingTemplate messagingTemplate; @GetMapping("/push") public String pushToAll() { // 向所有订阅了 /topic/news 的客户端推送消息 messagingTemplate.convertAndSend("/topic/news", "突发新闻:Spring Boot 3.0发布!"); return "推送成功"; } @GetMapping("/push/user") public String pushToUser(String userId) { // 向特定用户推送消息,用户订阅了 /user/{userId}/message 才能收到 messagingTemplate.convertAndSendToUser(userId, "/message", "您有一条私信"); return "私信推送成功"; } }
convertAndSend用于广播,convertAndSendToUser用于点对点。注意convertAndSendToUser默认会拼接成/user/{userId}/message这样的目的地,用户客户端需要订阅/user/message(实际上前缀/user会被处理,后面详述)。
6.5 前端使用SockJS和STOMP.js 前端需要引入SockJS和STOMP.js库。可以通过CDN或本地文件引入。
连接端点:new SockJS('/ws-stomp'),对应服务端注册的端点。
连接成功后,订阅/topic/greetings和/user/message。
发送消息:stompClient.send("/app/hello", {}, JSON.stringify({ 'name': name })),目的地址为/app/hello。
收到消息后解析并显示。
6.6 广播与点对点的深入理解
6.6.1 广播(Topic) 广播模式:所有订阅了某个主题的客户端都会收到消息。在配置中我们启用了简单消息代理,并指定了前缀/topic。任何发送到/topic/**的消息都会被转发给所有订阅了该目的地的客户端。例如:
服务器调用convertAndSend("/topic/news", "内容"),所有订阅/topic/news的客户端都会收到。
6.6.2 点对点(Queue) 点对点模式:消息只发送给特定的用户。Spring内部通过/user/{username}/**这样的目的地来实现。客户端订阅时需要订阅/user/queue/message,服务器发送时使用convertAndSendToUser(username, "/queue/message", payload)。Spring会自动将/user前缀转换为用户特定的目的地。
注意:点对点需要知道当前用户是谁。通常结合Spring Security,通过认证的用户信息来确定用户标识。如果没有Spring Security,也可以在连接时传递用户信息,但比较复杂。
6.6.3 使用@SendToUser 在@MessageMapping方法上可以使用@SendToUser,表示将返回值发送给当前发送消息的用户(而不是广播)。例如:
@MessageMapping("/private") @SendToUser("/queue/private") public String handlePrivate(String message) { return "这是你的私密回声:" + message; }
客户端需要订阅/user/queue/private来接收。
7. 安全集成(Spring Security + WebSocket) 在实际应用中,WebSocket连接往往需要鉴权,确保只有合法用户才能连接和订阅某些目的地。Spring Security提供了与WebSocket的集成。
7.1 添加Spring Security依赖 org.springframework.boot spring-boot-starter-security
7.2 配置Spring Security 创建一个简单的Security配置类,启用基本认证,并设置用户内存存储用于测试。
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.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; 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() // WebSocket端点需要认证 .anyRequest().permitAll() .and() .formLogin() .permitAll() .and() .logout() .permitAll() .and() .csrf().disable(); // 简单起见禁用CSRF,生产环境需配置 return http.build(); } @Bean public UserDetailsService userDetailsService() { InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(User.withUsername("user1").password("{noop}password1").roles("USER").build()); manager.createUser(User.withUsername("user2").password("{noop}password2").roles("USER").build()); return manager; } }
注意 :{noop}表示明文密码,仅为演示,生产环境应使用加密。
7.3 WebSocket拦截器获取用户信息 为了让WebSocket知道当前用户是谁,我们需要在握手阶段将认证信息传递进去。Spring提供了DefaultHandshakeHandler,我们可以重写determineUser方法。更常见的做法是结合Spring Security,在连接时通过Cookie或Header传递token,然后解析。
在STOMP配置中,我们可以添加一个ChannelInterceptor,用于在消息进入时设置用户。
import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.ChannelRegistration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; 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"); } @Override public void configureClientInboundChannel(ChannelRegistration registration) { // 添加自定义的拦截器,用于从消息中提取用户信息 registration.interceptors(new UserInterceptor()); } }
UserInterceptor需要实现ChannelInterceptor接口,重写preSend方法,从消息头中获取认证信息,设置到Message中。
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); // 如果是连接帧,可以从header中获取token,然后设置认证用户 if (accessor != null && accessor.getCommand() != null) { switch (accessor.getCommand()) { case CONNECT: // 假设客户端在连接时通过header传递了token,例如 "Authorization: Bearer xxx" String token = accessor.getFirstNativeHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); // 解析token获取用户信息,此处简化:根据token查数据库或JWT解析 // 假设解析出用户名为user1 // 然后创建Authentication对象 // 这里简单模拟:从内存中获取用户 // 实际中需要从JWT解析出用户名,再加载UserDetails // 然后创建UsernamePasswordAuthenticationToken // 并将authentication设置到SecurityContext中 // 同时设置accessor.setUser(authentication) // ... } break; case SUBSCRIBE: // 可以检查订阅目的地权限 break; // 其他命令... } } return message; } }
一种更简单的做法是:如果已经通过HTTP登录(如使用表单登录),那么在WebSocket握手时,由于同源,JSESSIONID Cookie会自动携带,Spring Security会识别出已登录的用户,自动将用户信息绑定到WebSocket会话上。只要连接路径与HTTP共享相同的会话,就可以。因此,很多情况下不需要额外配置,只需确保WebSocket端点在安全配置中允许已认证用户访问即可。
7.4 基于角色的目的地权限控制 除了认证,我们还可能需要对目的地进行权限控制,比如只有管理员才能订阅/topic/admin。Spring Security提供了@PreAuthorize等注解,但需要配合消息拦截器。
我们可以扩展ChannelInterceptor,在preSend中检查目标地址和用户角色。
@Override public Message preSend(Message message, MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); if (accessor != null && StompCommand.SUBSCRIBE.equals(accessor.getCommand())) { String destination = accessor.getDestination(); Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (destination != null && destination.startsWith("/topic/admin") && (auth == null || !auth.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_ADMIN")))) { // 没有权限,拒绝订阅 throw new AccessDeniedException("无权限订阅此主题"); } } return message; }
8. 集群环境下的WebSocket 在生产环境中,应用通常会部署多个实例以实现高可用和负载均衡。WebSocket连接是状态化的(每个连接对应一个会话),在集群环境下会面临会话共享问题:用户连接到实例A,但推送消息时可能由实例B发送,实例B没有该用户的会话信息,导致推送失败。
8.1 问题分析
WebSocket会话(WebSocketSession)保存在每个节点的内存中,其他节点无法访问。
当使用简单消息代理(enableSimpleBroker)时,消息只在当前节点内分发,无法跨节点广播给连接到其他节点的客户端。
点对点消息也可能发送到错误的节点。
8.2 解决方案概览
使用外部消息代理 :如RabbitMQ、ActiveMQ、Kafka等,作为消息的中转站。所有节点都连接到同一个消息代理,订阅相关的主题或队列。当某个节点需要推送消息时,将消息发送到消息代理,代理再将消息广播给所有订阅了该主题的节点,各节点再将消息推送给各自的客户端。
会话信息集中存储 :将会话信息(如用户与节点的映射关系)存储在Redis等集中式缓存中,推送时根据用户找到对应的节点,然后通过HTTP或RPC转发推送请求。但这种方式实现较复杂,不如使用消息代理直接。
使用Stomp Broker Relay :Spring提供了StompBrokerRelayMessageHandler,可以配置将消息转发给外部的STOMP代理(如RabbitMQ的STOMP插件)。
8.3 使用RabbitMQ作为外部消息代理
8.3.1 安装并启用RabbitMQ STOMP插件 RabbitMQ默认不支持STOMP,需要启用插件:
rabbitmq-plugins enable rabbitmq_stomp
8.3.2 修改Spring Boot配置 org.springframework.boot spring-boot-starter-amqp
修改StompWebSocketConfig,使用StompBrokerRelay代替简单的内存代理:
@Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.setApplicationDestinationPrefixes("/app"); // 使用RabbitMQ作为外部消息代理 registry.enableStompBrokerRelay("/topic", "/queue") .setRelayHost("localhost") .setRelayPort(61613) // STOMP插件默认端口 .setClientLogin("guest") .setClientPasscode("guest") .setSystemLogin("guest") .setSystemPasscode("guest"); registry.setUserDestinationPrefix("/user"); }
这样配置后,消息的广播和点对点都将通过RabbitMQ进行分发,所有应用节点都连接到同一个RabbitMQ,从而实现跨节点通信。
8.3.3 注意事项
需要确保RabbitMQ的STOMP插件监听端口(默认为61613)可访问。
生产环境需配置正确的用户名密码,并考虑使用SSL。
集群节点数量较多时,消息代理的性能成为瓶颈,需合理配置。
8.4 使用Redis存储会话 另一种思路是不用消息代理,而是将会话信息存储在Redis中。推送时,根据用户ID从Redis查找该用户当前连接到哪个节点(存储节点地址),然后通过HTTP调用该节点的API进行推送。这种方式需要自己实现节点间通信,但比较灵活。
不过,这种方法会引入额外的网络开销,且需要处理节点故障时的会话迁移。相比消息代理,实现复杂度更高。通常推荐使用消息代理方案。
9. 性能优化与最佳实践
9.1 心跳机制 WebSocket本身有Ping/Pong帧用于心跳检测,保持连接不超时。STOMP也支持心跳,客户端和服务器可以协商心跳间隔。在Spring中,可以配置心跳:
registry.enableSimpleBroker("/topic", "/queue") .setHeartbeatValue(new long[]{10000, 10000}); // 客户端和服务器均每10秒发送一次心跳
对于StompBrokerRelay,也需要配置心跳。
9.2 线程模型 Spring WebSocket使用任务线程池处理消息。默认情况下,@EnableWebSocketMessageBroker会配置一个ThreadPoolTaskExecutor。我们可以自定义线程池参数,防止高并发下线程耗尽。
@Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.taskExecutor() .corePoolSize(10) .maxPoolSize(20) .keepAliveSeconds(60); } @Override public void configureClientOutboundChannel(ChannelRegistration registration) { registration.taskExecutor() .corePoolSize(10) .maxPoolSize(20) .keepAliveSeconds(60); }
9.3 消息大小限制 WebSocket协议本身对消息大小没有严格限制,但底层实现和代理可能有默认限制。在Spring中,可以通过WebSocketTransportProperties调整,或者配置StompBrokerRelay的messageSizeLimit。
@Bean public AbstractBrokerMessageHandler stompBrokerRelayMessageHandler( StompBrokerRelayRegistration registration, MessageBrokerProperties properties) { StompBrokerRelayMessageHandler handler = new StompBrokerRelayMessageHandler(registration); handler.setMessageSizeLimit(1024 * 1024); // 1MB return handler; }
9.4 连接数限制 WebSocket是基于TCP的长连接,每个连接都会占用服务器资源。操作系统有最大文件描述符限制,应用服务器也有相关配置。可以通过以下方式优化:
使用Nginx等反向代理负载均衡,分散连接压力。
调整Tomcat的最大连接数(server.tomcat.max-connections)。
合理设置心跳,及时清理僵尸连接。
9.5 压缩与编码 对于大量数据的传输,可以考虑使用二进制格式(如Protocol Buffers、MessagePack)代替JSON,减少数据大小。Spring支持自定义消息转换器。
10. 测试WebSocket
10.1 使用浏览器开发者工具 现代浏览器(Chrome、Firefox)的开发者工具中,可以在'Network'标签查看WebSocket帧,或者使用专门的WebSocket测试插件。
10.2 使用命令行工具 如websocat、wscat(Node.js)等。例如:
wscat -c ws://localhost:8080/ws/echo
10.3 Spring Boot集成测试 Spring提供了WebSocketStompClient用于编写测试。我们可以模拟客户端连接、发送和接收消息。
import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.messaging.converter.MappingJackson2MessageConverter; import org.springframework.messaging.simp.stomp.StompFrameHandler; import org.springframework.messaging.simp.stomp.StompHeaders; import org.springframework.messaging.simp.stomp.StompSession; import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter; import org.springframework.web.socket.client.standard.StandardWebSocketClient; import org.springframework.web.socket.messaging.WebSocketStompClient; import java.lang.reflect.Type; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class WebSocketTest { @Test void testGreeting() throws Exception { WebSocketStompClient stompClient = new WebSocketStompClient(new StandardWebSocketClient()); stompClient.setMessageConverter(new MappingJackson2MessageConverter()); String url = "ws://localhost:" + port + "/ws-stomp"; StompSession session = stompClient.connect(url, new StompSessionHandlerAdapter() {}).get(1, TimeUnit.SECONDS); CompletableFuture future = new CompletableFuture<>(); session.subscribe("/topic/greetings", new StompFrameHandler() { @Override public Type getPayloadType(StompHeaders headers) { return Greeting.class; } @Override public void handleFrame(StompHeaders headers, Object payload) { future.complete((Greeting) payload); } }); session.send("/app/hello", new HelloMessage("TestUser")); Greeting greeting = future.get(5, TimeUnit.SECONDS); assertThat(greeting.getContent()).contains("Hello, TestUser"); } // 需要注入端口 @LocalServerPort private int port; }
11. 常见问题与解决方案
11.1 连接失败(404)
检查端点路径是否正确,包括上下文路径(server.servlet.context-path)。
检查是否启用了SockJS,前端连接时是否使用了正确路径。
如果使用STOMP,确认配置了setAllowedOrigins,防止跨域问题。
11.2 消息丢失
确认客户端订阅了正确的目的地。
如果是点对点消息,确保convertAndSendToUser中的用户名正确,且客户端订阅了/user/...。
检查网络或代理是否关闭了连接。
11.3 断线重连 前端STOMP客户端可以监听断开事件,并重新连接。例如:
stompClient.connect({}, function(frame) { // 连接成功 }, function(error) { // 连接失败,延时重连 setTimeout(connect, 5000); });
11.4 跨域问题 在registerStompEndpoints中通过setAllowedOrigins("*")允许所有域名,生产环境应指定具体域名。注意,SockJS不支持*,需要使用具体域名或使用setAllowedOriginPatterns("*")。
11.5 客户端无法接收广播消息
确保客户端订阅了正确的主题,如/topic/news。
检查服务器推送的目的地是否一致。
如果使用外部消息代理,检查代理配置。
11.6 性能瓶颈
如果单节点连接数过多,考虑水平扩展并使用消息代理。
调整JVM内存和GC参数。
使用异步、非阻塞IO。
12. 实战案例:实时通知系统 下面我们通过一个简单的实时通知系统,综合运用上述知识。
12.1 需求描述
系统广播:所有在线用户都能收到的通知(如系统维护公告)。
个人私信:仅特定用户收到的通知(如评论回复)。
12.2 后端实现
配置类 :使用STOMP over WebSocket,配置端点、消息代理。
安全配置 :集成Spring Security,使用内存用户。
通知实体 :
public class Notification { private String type; // "BROADCAST" 或 "PRIVATE" private String content; private String targetUser; // 私信目标用户,广播时为空 private Date timestamp; // 构造、getter、setter }
Controller处理私信发送(来自其他用户) :
@MessageMapping("/private") public void sendPrivate(@Payload Notification notification, Principal 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 "广播成功"; } }
12.3 前端实现 登录页面(省略),假设登录后跳转到主页。主页HTML包含:
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); }); }); }
12.4 测试
启动应用,用两个不同用户登录(如user1和user2)。
调用广播API,所有在线用户都能看到通知。
发送私信给user1,只有user1能看到。
13. 总结与展望 本文从WebSocket的基本概念出发,详细介绍了在Spring Boot中集成WebSocket的两种方式:原生WebSocket和基于STOMP的消息代理方式。我们探讨了消息的广播与点对点推送、安全集成、集群部署以及性能优化。通过实战案例,展示了如何构建一个实时通知系统。
WebSocket技术为实时Web应用提供了强大的支持,结合Spring Boot的便捷性,开发者可以快速构建出高实时性的应用。随着Web技术的不断发展,WebSocket的应用场景将越来越广泛,比如在线游戏、协同编辑、实时监控等
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 curl 转代码 解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online