什么是 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
):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
:yamlspring: profiles: active: dev freemarker: template-loader-path: classpath:/templates/ suffix: .ftl cache: false activemq: broker-url: tcp://localhost:61616 user: admin password: admin server: port: 8081 springdoc: api-docs: path: /api-docs swagger-ui: path: /swagger-ui.html
2. 实现 WebSocket 聊天应用
以下是一个简单的实时聊天应用示例,使用 STOMP over WebSocket。
-
WebSocket 配置:
javapackage 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 { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic"); // 广播消息 config.setApplicationDestinationPrefixes("/app"); // 客户端发送消息前缀 } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/chat").withSockJS(); // WebSocket 端点,兼容 SockJS } }
-
消息控制器:
javapackage 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; // 广播消息 } }
-
消息实体:
javapackage com.example.demo.controller; public class ChatMessage { private String content; private String sender; // Getters and Setters public 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
):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>
-
控制器:
javapackage 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、异常处理):
-
分页与排序:
-
实时显示分页用户数据:
java@MessageMapping("/fetchUsers") @SendTo("/topic/users") public Page<User> fetchUsers(@Payload UserFilter filter) { return userService.searchUsers(filter.getName(), filter.getPage(), filter.getSize(), "id", "asc"); }
javapublic class UserFilter { private String name; private int page; private int size; // Getters and Setters }
-
-
Swagger:
-
文档化 REST API,非 WebSocket:
java@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"); }
-
-
ActiveMQ:
-
记录聊天消息:
java@Controller public class ChatController { @Autowired private JmsTemplate jmsTemplate; @MessageMapping("/sendMessage") @SendTo("/topic/messages") public ChatMessage sendMessage(ChatMessage message) { jmsTemplate.convertAndSend("chat-log", message.getSender() + ": " + message.getContent()); return message; } }
-
-
Spring Profiles:
-
配置开发/生产环境:
yaml# application-dev.yml spring: freemarker: cache: false logging: level: root: DEBUG
yaml# application-prod.yml spring: freemarker: cache: true
-
-
Spring Security:
-
保护 WebSocket 连接:
java@Configuration public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/chat").authenticated() .anyRequest().permitAll() ) .formLogin() .and() .csrf().ignoringRequestMatchers("/chat"); // WebSocket 端点禁用 CSRF return http.build(); } }
-
-
Spring Batch:
-
批量推送用户数据:
java@Component public class BatchConfig { @Bean public 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:
yamlspring: devtools: restart: enabled: true
-
-
ThreadLocal:
-
清理 WebSocket 上下文:
java@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(); } } }
-
-
Actuator 安全性:
- 保护
/actuator/**
,允许/actuator/health
。
- 保护
-
CSRF:
- WebSocket 端点禁用 CSRF(见
SecurityConfig
)。
- WebSocket 端点禁用 CSRF(见
-
异常处理:
-
处理 WebSocket 异常:
java@ControllerAdvice public class WebSocketExceptionHandler { @ExceptionHandler(MessagingException.class) public void handleMessagingException(MessagingException ex, SimpMessageHeaderAccessor headerAccessor) { headerAccessor.getSessionAttributes().put("error", ex.getMessage()); } }
-
4. 运行验证
-
开发环境:
bashjava -jar demo.jar --spring.profiles.active=dev
- 访问
http://localhost:8081/chat
,登录后发送消息,验证实时性。 - 检查 ActiveMQ
chat-log
队列。
- 访问
-
生产环境:
bashjava -jar demo.jar --spring.profiles.active=prod
- 确认安全性和模板缓存。
原理与性能
原理
- WebSocket 协议:基于 TCP,使用 HTTP 握手升级。
- STOMP:在 WebSocket 上添加消息路由,支持订阅/发布。
- Spring WebSocket:管理连接、消息路由和广播。
性能
- 连接建立:50ms(单用户)。
- 消息传输:1-2ms/消息。
- 并发:1000 用户,延迟 <10ms(8 核 CPU,16GB 内存)。
测试
java
@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 解决。