WebSocket 详解 + 高级实战用法(Spring Boot + Java)
视频看了几百小时还迷糊?关注我,几分钟让你秒懂!
一、为什么需要 WebSocket?
传统的 Web 应用基于 HTTP 协议,是请求-响应模式:
- 客户端发请求 → 服务端处理 → 返回结果 → 连接关闭
- 如果服务端有新数据(如聊天消息、订单状态变更),无法主动推给客户端
为实现“实时通信”,早期常用 轮询(Polling) 或 长轮询(Long Polling):
- 轮询:每隔几秒发一次请求 → 浪费带宽、延迟高
- 长轮询:请求挂起直到有数据 → 仍基于 HTTP,连接频繁建立/断开
✅ WebSocket 的优势:
- 全双工通信:客户端和服务端可随时互相发送数据
- 单 TCP 连接:握手后复用连接,低开销
- 低延迟、高效率:适合实时场景(聊天、股票、游戏、监控)
二、WebSocket 协议简析
- 连接建立:后续通信使用 WebSocket 帧(Frame),不再是 HTTP
服务端响应:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= 握手阶段:客户端通过 HTTP 发起 Upgrade 请求
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13 🔑 关键:一次握手,长期通信
三、Spring Boot 整合 WebSocket(基础版)
场景:简易在线聊天室
1. 添加依赖(pom.xml)
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> 2. 启用 WebSocket 支持
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { // 注册 STOMP 端点,允许跨域 registry.addEndpoint("/ws") .setAllowedOriginPatterns("*") .withSockJS(); // 兼容不支持 WebSocket 的浏览器(降级到 xhr-streaming) } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 客户端订阅前缀 registry.enableSimpleBroker("/topic", "/queue"); // 服务端推送前缀 registry.setApplicationDestinationPrefixes("/app"); // 客户端发送消息前缀 } } 💡 使用 STOMP(Simple Text Oriented Messaging Protocol)作为子协议,简化消息路由。
3. 编写控制器(处理客户端消息)
@Controller public class ChatController { @MessageMapping("/chat.sendMessage") // 客户端发送到 /app/chat.sendMessage @SendTo("/topic/public") // 广播给所有订阅 /topic/public 的客户端 public ChatMessage sendMessage(ChatMessage chatMessage) { return chatMessage; } @MessageMapping("/chat.addUser") @SendTo("/topic/public") public ChatMessage addUser(@Payload ChatMessage chatMessage, SimpMessageHeaderAccessor headerAccessor) { // 将用户名加入会话 headerAccessor.getSessionAttributes().put("username", chatMessage.getSender()); chatMessage.setContent("加入聊天室"); return chatMessage; } } 4. 定义消息实体
@Data @NoArgsConstructor @AllArgsConstructor public class ChatMessage { private String content; private String sender; private MessageType type; // enum: CHAT, JOIN, LEAVE public enum MessageType { CHAT, JOIN, LEAVE } } 5. 前端连接(HTML + JS)
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/stomp.min.js"></script> <script> const stompClient = Stomp.over(new SockJS('/ws')); stompClient.connect({}, () => { console.log('连接成功'); // 订阅公共频道 stompClient.subscribe('/topic/public', (payload) => { const msg = JSON.parse(payload.body); document.getElementById('chat').innerHTML += `<div><b>${msg.sender}</b>: ${msg.content}</div>`; }); // 发送消息 function sendMessage() { const content = document.getElementById('message').value; stompClient.send("/app/chat.sendMessage", {}, JSON.stringify({ content: content, sender: '张三', type: 'CHAT' })); } }); </script> ✅ 效果:用户加入、发消息,所有在线用户实时看到。
四、高级实战用法
场景1:私聊(点对点通信)
❌ 反例:用 /topic 广播实现私聊 → 信息泄露!
✅ 正确做法:使用 用户专属队列
// 获取当前登录用户(需集成 Spring Security) private String getCurrentUser(SimpMessageHeaderAccessor headerAccessor) { // 从 token / session 中解析 return (String) headerAccessor.getSessionAttributes().get("username"); } @MessageMapping("/chat.private") public void sendPrivateMessage(@Payload ChatMessage message, SimpMessageHeaderAccessor headerAccessor) { String currentUser = getCurrentUser(headerAccessor); String recipient = message.getRecipient(); // 假设消息中包含接收者 // 构造用户专属队列路径 String destination = "/queue/messages/" + recipient; // 使用 convertAndSendToUser 发送(自动加 /user 前缀) messagingTemplate.convertAndSendToUser(recipient, "/queue/messages", message); } ⚠️ 注意:前端需订阅 /user/queue/messages(STOMP 会自动映射到用户会话)// 前端订阅私信 stompClient.subscribe('/user/queue/messages', (payload) => { const msg = JSON.parse(payload.body); // 显示私聊消息 }); 场景2:连接管理 & 在线人数统计
@Component public class WebSocketEventListener { private static final Set<String> onlineUsers = ConcurrentHashMap.newKeySet(); @EventListener public void handleWebSocketConnectListener(SessionConnectedEvent event) { // 用户上线(需结合认证) System.out.println("用户连接: " + event.getMessage().getHeaders()); } @EventListener public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) { StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage()); String username = (String) headerAccessor.getSessionAttributes().get("username"); if (username != null) { onlineUsers.remove(username); // 广播在线人数 messagingTemplate.convertAndSend("/topic/online", onlineUsers.size()); } } } 可用于“好友在线状态”、“客服排队人数”等场景。
场景3:消息持久化 & 离线消息
当用户离线时,消息不能丢!
方案:
- 发送前检查用户是否在线(维护
Map<userId, sessionId>) - 若不在线,将消息存入数据库(
offline_messages表) - 用户上线时,拉取离线消息并推送
public void sendPrivateMessage(ChatMessage message) { if (isUserOnline(message.getRecipient())) { messagingTemplate.convertAndSendToUser(...); } else { offlineMessageService.save(message); // 存 DB } } // 用户上线时 @EventListener public void onUserOnline(SessionConnectedEvent event) { String userId = getCurrentUserId(event); List<ChatMessage> offlineMsgs = offlineMessageService.getAndDelete(userId); offlineMsgs.forEach(msg -> messagingTemplate.convertAndSendToUser(userId, "/queue/offline", msg) ); } 五、注意事项 & 常见坑点
| 问题 | 解决方案 |
|---|---|
| 跨域问题 | setAllowedOriginPatterns("*")(生产环境应限制域名) |
| 集群部署 | 单机内存存储会话 → 多节点无法互通 → 需用 Redis + WebSocket 消息广播 |
| 连接断开重连 | 前端监听 onclose,自动重连 + 重新订阅 |
| 消息丢失 | 关键消息需 ACK 机制 + 重试(WebSocket 本身不保证可靠) |
| 安全认证 | 握手时校验 Token(通过 HandshakeInterceptor) |
示例:Token 认证拦截器
public class HttpHandshakeInterceptor implements HandshakeInterceptor { @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) { if (request instanceof ServletServerHttpRequest) { ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request; String token = servletRequest.getServletRequest().getParameter("token"); if (isValidToken(token)) { attributes.put("userId", parseUserId(token)); return true; } } return false; // 拒绝连接 } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {} } 注册到配置中:
registry.addEndpoint("/ws") .addInterceptors(new HttpHandshakeInterceptor()) .setAllowedOriginPatterns("*"); 六、替代方案对比
| 技术 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| WebSocket | 真正双向、低延迟 | 需要保持长连接 | 聊天、实时协作、游戏 |
| SSE (Server-Sent Events) | 简单、基于 HTTP | 仅服务端→客户端 | 实时通知、日志流 |
| MQTT over WebSocket | 轻量、支持 QoS | 需要额外 Broker | IoT 设备通信 |
| gRPC-Web | 高性能、强类型 | 浏览器支持有限 | 内部微服务调用 |
七、总结
WebSocket 是构建实时 Web 应用的核心技术,关键要点:
- 使用 STOMP + Spring Message Broker 简化开发
- 私聊用
/user/queue,广播用/topic - 连接状态管理 实现在线人数、离线消息
- 集群部署需 Redis 广播(如
spring-boot-starter-redis+RedisMessageListener) - 安全第一:握手时校验身份,避免未授权连接
视频看了几百小时还迷糊?关注我,几分钟让你秒懂!