WebSocket 详解 + 高级实战用法(Spring Boot + Java)

视频看了几百小时还迷糊?关注我,几分钟让你秒懂!

一、为什么需要 WebSocket?

传统的 Web 应用基于 HTTP 协议,是请求-响应模式:

  • 客户端发请求 → 服务端处理 → 返回结果 → 连接关闭
  • 如果服务端有新数据(如聊天消息、订单状态变更),无法主动推给客户端

为实现“实时通信”,早期常用 轮询(Polling)长轮询(Long Polling)

  • 轮询:每隔几秒发一次请求 → 浪费带宽、延迟高
  • 长轮询:请求挂起直到有数据 → 仍基于 HTTP,连接频繁建立/断开

WebSocket 的优势

  • 全双工通信:客户端和服务端可随时互相发送数据
  • 单 TCP 连接:握手后复用连接,低开销
  • 低延迟、高效率:适合实时场景(聊天、股票、游戏、监控)

二、WebSocket 协议简析

  1. 连接建立:后续通信使用 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:消息持久化 & 离线消息

当用户离线时,消息不能丢!

方案:
  1. 发送前检查用户是否在线(维护 Map<userId, sessionId>
  2. 若不在线,将消息存入数据库(offline_messages 表)
  3. 用户上线时,拉取离线消息并推送
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需要额外 BrokerIoT 设备通信
gRPC-Web高性能、强类型浏览器支持有限内部微服务调用

七、总结

WebSocket 是构建实时 Web 应用的核心技术,关键要点:

  • 使用 STOMP + Spring Message Broker 简化开发
  • 私聊用 /user/queue,广播用 /topic
  • 连接状态管理 实现在线人数、离线消息
  • 集群部署需 Redis 广播(如 spring-boot-starter-redis + RedisMessageListener
  • 安全第一:握手时校验身份,避免未授权连接
视频看了几百小时还迷糊?关注我,几分钟让你秒懂!

Read more

【Python基础:语法第一课】Python 基础语法详解:变量、类型、动态特性与运算符实战,构建完整的编程基础认知体系

【Python基础:语法第一课】Python 基础语法详解:变量、类型、动态特性与运算符实战,构建完整的编程基础认知体系

🎬 个人主页:艾莉丝努力练剑 ❄专栏传送门:《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录》 《Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享》 ⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平 🎬 艾莉丝的简介: 文章目录 * 1 ~> 常量和表达式 * 2 ~> 变量和类型 * 2.1 变量是什么 * 2.2 变量的语法 * 2.2.1 定义变量 * 2.2.2 使用变量 * 2.3 变量的类型:对于不同种类的变量作出区分 * 2.3.1 整数 * 2.

五大经典排序算法:插入、希尔、冒泡、选择、堆排序全攻略

五大经典排序算法:插入、希尔、冒泡、选择、堆排序全攻略

目录 --------------插入排序------------- 1、插入排序思想 2、示例代码 3、效率分析 --------------希尔排序------------- 1、希尔排序思想 2、示例代码 3、效率分析 --------------选择排序------------- 1、选择排序思想 2、示例代码 3、效率分析 ---------------堆排序-------------- 1、堆排序思想 2、示例代码 3、效率分析 --------------冒泡排序------------- 1、冒泡排序思想 2、示例代码 3、效率分析 上述五大排序性能对比: --------------插入排序------------- 1、插入排序思想 插入排序的核心思想是逐步构建有序序列: 将数组分为 “已排序” 和 “未排序” 两部分,初始时已排序部分只包含第一个元素。 每次从未排序部分取出第一个元素,将其向前插入到已排序序列中的正确位置,使得插入后的序列依然保持有序。

Python快速入门专业版(十四):变量赋值的“陷阱”:浅拷贝与深拷贝(用代码看懂内存地址)

Python快速入门专业版(十四):变量赋值的“陷阱”:浅拷贝与深拷贝(用代码看懂内存地址)

目录 引言:为什么改了b,a也跟着变? 1.赋值的本质:不是值传递,而是引用传递 1.1 用id()函数看穿内存地址 场景1:不可变对象的赋值(无副作用) 场景2:可变对象的赋值(有副作用) 1.2 不可变对象的“特殊情况”:小整数池与字符串驻留 2.浅拷贝(Shallow Copy):只复制“外层壳子” 2.1 浅拷贝的4种实现方式 代码示例:列表的浅拷贝 2.2 浅拷贝的“隐形陷阱”:内层对象仍共享 代码演示:浅拷贝的内层共享问题 2.3 浅拷贝的适用场景 3.深拷贝(Deep Copy):复制“所有层级”

Python + Ursina设计3D小游戏设计基础知识

Python + Ursina设计3D小游戏设计基础知识

Python + Ursina设计3D小游戏设计基础知识 本文介绍了Ursina的安装方法、核心概念(实体、模型、坐标系等)和基本使用流程,Ursina坐标系知识,以及实用技巧代码示例。 一、Ursina介绍 Ursina:一个基于Panda3D的Python游戏引擎,简化了3D游戏开发。 Ursina本身:约5-15 MB。安装Ursina时,pip会自动安装其依赖,包括Panda3D、pygame、numpy等。这些依赖库加起来可能会占用几百MB的磁盘空间。但具体占用空间大小取决于你的系统以及是否已经安装了一些依赖。与传统游戏引擎相比还是比较小的。显著优点是简单易学,Ursina的API设计非常直观,适合初学者快速上手3D游戏开发。可以跨平台,即可在Windows、Mac、Linux上运行。但不适合大型或高性能游戏。 ursina/                         5-15 MB panda3d/                      200-300 MB pygame/                          5-10 MB