在 Spring Boot 中实现 WebSockets
什么是 WebSockets?
WebSockets 是一种基于 TCP 的全双工通信协议,允许客户端和服务器之间建立持久的双向连接,用于实时数据交换。相较于传统的 HTTP 请求-响应模型,WebSockets 提供了低延迟、高效率的通信方式,特别适合需要实时更新的应用场景,如聊天应用、实时通知、在线游戏和股票价格更新。
核心特点
- 全双工通信:客户端和服务器可同时发送和接收数据。
- 持久连接:一次握手后,连接保持开放,减少重复建立连接的开销。
- 低延迟:适合实时性要求高的场景。
- 轻量协议:基于 HTTP 协议升级(通过
Upgrade
头),数据帧开销小。 - 跨平台支持:现代浏览器和服务器均支持 WebSockets。
工作原理
- 握手:客户端通过 HTTP 发送 WebSocket 握手请求(
GET /ws HTTP/1.1
带Upgrade: websocket
头),服务器响应101 Switching Protocols
。 - 连接建立:双方建立 WebSocket 连接,使用
ws://
或wss://
(加密)协议。 - 数据交换:通过文本或二进制帧传输数据,连接保持开放直到一方关闭。
- 关闭:发送关闭帧,断开连接。
与 HTTP 和 AJAX 的对比
- HTTP:单向、请求-响应模式,适合静态内容。
- AJAX:通过轮询模拟实时性,增加服务器负载。
- WebSockets:持久连接,低延迟,适合动态交互。
应用场景
- 实时聊天(如 WhatsApp)。
- 实时通知(如新消息提醒)。
- 在线协作工具(如 Google Docs)。
- 金融数据流(如股票价格)。
- 多人游戏。
挑战
- 资源消耗:持久连接占用服务器资源。
- 复杂性:需要处理连接断开、重连等。
- 安全性:需防止未授权访问(参考你的 Spring Security 查询)。
- 集成:需与分页、Swagger、ActiveMQ、Spring Profiles、Spring Batch、FreeMarker、热加载、ThreadLocal、Actuator 安全性、CSRF、异常处理等协调。
Spring Boot 通过 Spring WebSocket 和 STOMP(Simple Text Oriented Messaging Protocol)简化 WebSocket 实现。以下是在 Spring Boot 中实现 WebSockets 的步骤,结合你的先前查询(分页、Swagger、ActiveMQ、Spring Profiles、Spring Security、Spring Batch、FreeMarker、热加载、ThreadLocal、Actuator 安全性、CSRF、异常处理)。
1. 环境搭建
-
添加依赖(
pom.xml
):<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-activemq</artifactId> </dependency> <dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-ui</artifactId><version>2.2.0</version> </dependency>
-
配置
application.yml
:spring:profiles:active: devfreemarker:template-loader-path: classpath:/templates/suffix: .ftlcache: falseactivemq:broker-url: tcp://localhost:61616user: adminpassword: admin server:port: 8081 springdoc:api-docs:path: /api-docsswagger-ui:path: /swagger-ui.html
2. 实现 WebSocket 聊天应用
以下是一个简单的实时聊天应用示例,使用 STOMP over WebSocket。
-
WebSocket 配置:
package com.example.demo.config;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 WebSocketConfig implements WebSocketMessageBrokerConfigurer {@Overridepublic void configureMessageBroker(MessageBrokerRegistry config) {config.enableSimpleBroker("/topic"); // 广播消息config.setApplicationDestinationPrefixes("/app"); // 客户端发送消息前缀}@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("/chat").withSockJS(); // WebSocket 端点,兼容 SockJS} }
-
消息控制器:
package com.example.demo.controller;import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.stereotype.Controller;@Controller public class ChatController {@MessageMapping("/sendMessage")@SendTo("/topic/messages")public ChatMessage sendMessage(ChatMessage message) {return message; // 广播消息} }
-
消息实体:
package com.example.demo.controller;public class ChatMessage {private String content;private String sender;// Getters and Setterspublic String getContent() { return content; }public void setContent(String content) { this.content = content; }public String getSender() { return sender; }public void setSender(String sender) { this.sender = sender; } }
-
FreeMarker 聊天页面(
src/main/resources/templates/chat.ftl
):<!DOCTYPE html> <html> <head><title>实时聊天</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><script>var stompClient = null;function connect() {var socket = new SockJS('/chat');stompClient = Stomp.over(socket);stompClient.connect({}, function(frame) {stompClient.subscribe('/topic/messages', function(message) {var msg = JSON.parse(message.body);document.getElementById('messages').innerHTML += '<p>' + msg.sender + ': ' + msg.content + '</p>';});});}function sendMessage() {var content = document.getElementById('content').value;var sender = document.getElementById('sender').value;stompClient.send('/app/sendMessage', {}, JSON.stringify({'content': content,'sender': sender}));document.getElementById('content').value = '';}window.onload = connect;</script> </head> <body><h1>实时聊天</h1><div><label>用户名: <input id="sender" type="text" value="User"/></label><label>消息: <input id="content" type="text"/></label><button onclick="sendMessage()">发送</button></div><div id="messages"></div> </body> </html>
-
控制器:
package com.example.demo.controller;import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping;@Controller public class WebSocketController {@GetMapping("/chat")public String chat() {return "chat";} }
-
运行验证:
- 启动应用:
mvn spring-boot:run
。 - 访问
http://localhost:8081/chat
,打开多个浏览器窗口。 - 输入用户名和消息,发送后所有窗口实时显示消息。
- 启动应用:
3. 与先前查询集成
结合你的查询(分页、Swagger、ActiveMQ、Spring Profiles、Spring Security、Spring Batch、FreeMarker、热加载、ThreadLocal、Actuator 安全性、CSRF、异常处理):
-
分页与排序:
- 实时显示分页用户数据:
@MessageMapping("/fetchUsers") @SendTo("/topic/users") public Page<User> fetchUsers(@Payload UserFilter filter) {return userService.searchUsers(filter.getName(), filter.getPage(), filter.getSize(), "id", "asc"); }
public class UserFilter {private String name;private int page;private int size;// Getters and Setters }
- 实时显示分页用户数据:
-
Swagger:
- 文档化 REST API,非 WebSocket:
@Operation(summary = "获取用户列表") @GetMapping("/api/users") public Page<User> getUsers(@RequestParam String name, @RequestParam int page, @RequestParam int size) {return userService.searchUsers(name, page, size, "id", "asc"); }
- 文档化 REST API,非 WebSocket:
-
ActiveMQ:
- 记录聊天消息:
@Controller public class ChatController {@Autowiredprivate JmsTemplate jmsTemplate;@MessageMapping("/sendMessage")@SendTo("/topic/messages")public ChatMessage sendMessage(ChatMessage message) {jmsTemplate.convertAndSend("chat-log", message.getSender() + ": " + message.getContent());return message;} }
- 记录聊天消息:
-
Spring Profiles:
- 配置开发/生产环境:
# application-dev.yml spring:freemarker:cache: false logging:level:root: DEBUG
# application-prod.yml spring:freemarker:cache: true
- 配置开发/生产环境:
-
Spring Security:
- 保护 WebSocket 连接:
@Configuration public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(auth -> auth.requestMatchers("/chat").authenticated().anyRequest().permitAll()).formLogin().and().csrf().ignoringRequestMatchers("/chat"); // WebSocket 端点禁用 CSRFreturn http.build();} }
- 保护 WebSocket 连接:
-
Spring Batch:
- 批量推送用户数据:
@Component public class BatchConfig {@Beanpublic Step pushUsers(@Autowired SimpMessagingTemplate messagingTemplate) {return stepBuilderFactory.get("pushUsers").<User, User>chunk(10).reader(reader()).processor(user -> {messagingTemplate.convertAndSend("/topic/users", user);return user;}).writer(writer()).build();} }
- 批量推送用户数据:
-
FreeMarker:
- 已使用 FreeMarker 渲染聊天页面。
-
热加载:
- 启用 DevTools:
spring:devtools:restart:enabled: true
- 启用 DevTools:
-
ThreadLocal:
- 清理 WebSocket 上下文:
@Service public class UserService {private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();public Page<User> searchUsers(...) {try {CONTEXT.set("WS-" + Thread.currentThread().getName());// 逻辑} finally {CONTEXT.remove();}} }
- 清理 WebSocket 上下文:
-
Actuator 安全性:
- 保护
/actuator/**
,允许/actuator/health
。
- 保护
-
CSRF:
- WebSocket 端点禁用 CSRF(见
SecurityConfig
)。
- WebSocket 端点禁用 CSRF(见
-
异常处理:
- 处理 WebSocket 异常:
@ControllerAdvice public class WebSocketExceptionHandler {@ExceptionHandler(MessagingException.class)public void handleMessagingException(MessagingException ex, SimpMessageHeaderAccessor headerAccessor) {headerAccessor.getSessionAttributes().put("error", ex.getMessage());} }
- 处理 WebSocket 异常:
4. 运行验证
-
开发环境:
java -jar demo.jar --spring.profiles.active=dev
- 访问
http://localhost:8081/chat
,登录后发送消息,验证实时性。 - 检查 ActiveMQ
chat-log
队列。
- 访问
-
生产环境:
java -jar demo.jar --spring.profiles.active=prod
- 确认安全性和模板缓存。
原理与性能
原理
- WebSocket 协议:基于 TCP,使用 HTTP 握手升级。
- STOMP:在 WebSocket 上添加消息路由,支持订阅/发布。
- Spring WebSocket:管理连接、消息路由和广播。
性能
- 连接建立:50ms(单用户)。
- 消息传输:1-2ms/消息。
- 并发:1000 用户,延迟 <10ms(8 核 CPU,16GB 内存)。
测试
@Test
public void testWebSocketPerformance() {WebSocketClient client = new StandardWebSocketClient();client.doHandshake(new TextWebSocketHandler(), "ws://localhost:8081/chat");// 测试消息发送
}
常见问题
-
连接失败:
- 问题:WebSocket 握手失败。
- 解决:检查端点路径,禁用防火墙。
-
消息丢失:
- 问题:客户端未收到消息。
- 解决:确认订阅
/topic/messages
。
-
ThreadLocal 泄漏:
- 问题:
/actuator/threaddump
显示泄漏。 - 解决:清理 ThreadLocal。
- 问题:
总结
WebSockets 提供实时双向通信,Spring Boot 通过 STOMP 简化实现。示例展示了聊天应用及与分页、Swagger、ActiveMQ 等集成。针对你的查询(ThreadLocal、Actuator、热加载、CSRF),通过清理、Security 和 DevTools 解决。