引言:实时场景的"痛点",被WebSocket彻底解决
搞定接口文档标准化后,前后端联调效率大幅提升,但在开发实时交互场景时,我又遇到了新的瓶颈------传统的HTTP请求无法实现"服务器主动向客户端推送数据",只能靠客户端反复轮询(每隔几秒请求一次接口),踩了无数坑:
-
轮询效率极低:客户端每隔1秒请求一次接口,大部分请求都是"无效请求"(服务器数据未更新),浪费服务器资源和带宽,高并发场景下甚至会导致服务器卡顿;
-
实时性差:轮询间隔再短,也会有延迟(比如间隔1秒,最大延迟就是1秒),无法满足聊天、实时通知、订单状态推送等高频实时场景;
-
客户端负担重:频繁的请求会消耗客户端资源,移动端场景下会导致耗电快、流量消耗大,用户体验极差;
-
代码冗余复杂:需要单独编写轮询逻辑、处理请求频率、控制重试机制,后期维护成本高,新手容易写出bug。
印象最深的一次,我开发一个"在线聊天"功能,一开始用HTTP轮询实现:前端每隔1秒请求接口获取新消息,测试时发现,两个人聊天时会有明显延迟,消息发送后要等1秒才能看到,而且服务器压力极大,10个用户同时聊天,服务器每秒就要处理10次请求,再加上其他接口,直接导致服务器响应变慢。
后来,部门李哥告诉我:"实时交互场景,别用轮询,WebSocket才是最优解------它能建立客户端和服务器之间的长连接,实现双向实时通信,服务器能主动向客户端推送数据,无需客户端反复请求,实时性、效率都能提升一个档次。"
从那以后,我开始系统学习WebSocket的核心原理、实战用法,从最基础的概念、协议细节,到Spring Boot整合WebSocket、实现聊天/通知等场景,再到生产环境的高并发优化、故障排查,一步步摆脱了"轮询卡顿"的困境,慢慢学会了用WebSocket赋能实时交互场景。
今天,就把这段从"被轮询折磨"到"用WebSocket掌控实时交互"的成长历程,分享给和曾经的我一样,被实时场景困扰、想提升系统实时性的Java新手开发者。
注:本文聚焦Java后端WebSocket实战,从入门到精通,结合实际开发场景(在线聊天、实时通知、订单推送),全程用真实案例串联知识点,不冗余讲解理论,重点覆盖"基础原理+Spring Boot实战+多场景适配+生产优化+故障排查",所有代码和示例可直接复制使用,让你看完就能上手,真正用WebSocket解决实时交互痛点。
第一章:入门破局------吃透WebSocket基础,告别轮询卡顿
李哥告诉我:"WebSocket的核心,是'长连接+双向通信'------它打破了HTTP协议'请求-响应'的单向限制,客户端和服务器建立一次连接后,就能持续通信,服务器能主动向客户端推送数据,客户端也能随时向服务器发送请求,这也是它比轮询高效的核心原因。"
对于Java新手来说,WebSocket的进阶第一步,就是吃透以下基础知识点,快速摆脱轮询卡顿的困扰。
一、先搞懂:WebSocket到底是什么?(一看就懂)
很多新手觉得"WebSocket是一种新的HTTP协议",这是一个很大的误区。一句话讲明白WebSocket的定位:WebSocket是一种独立的应用层协议,基于TCP协议实现,专门用于解决"实时双向通信"场景,它能在客户端和服务器之间建立持久化的长连接,实现服务器主动向客户端推送数据,无需客户端反复发起请求。
举个通俗的例子:HTTP请求就像"打电话",客户端打给服务器(请求),服务器接电话(响应),通话结束(连接断开),下次再联系需要重新打电话;而WebSocket就像"视频通话",客户端和服务器建立一次连接(拨通视频),之后双方可以随时说话(双向通信),连接一直保持,直到主动挂断(关闭连接)。
补充:WebSocket的3个核心价值------
-
实时性强:服务器数据更新后,能立即推送给客户端,无轮询延迟,适合聊天、实时通知等场景;
-
效率高:建立一次长连接,无需反复发起请求,减少无效请求,节省服务器资源和带宽;
-
双向通信:客户端可向服务器发送请求,服务器也可主动向客户端推送数据,适配复杂实时交互场景。
现在,几乎所有实时交互场景(在线聊天、实时监控、订单状态推送、弹幕、股票行情),都在使用WebSocket,它已经成为Java后端开发必备的技能之一。
二、WebSocket vs HTTP(新手必懂,避免混淆)
新手最容易混淆WebSocket和HTTP,两者都是应用层协议,基于TCP协议,但定位和用法完全不同,一张表格讲清区别,新手可直接对照记忆:
| 对比维度 | HTTP | WebSocket |
|---|---|---|
| 通信方式 | 单向通信(客户端请求,服务器响应) | 双向通信(客户端↔服务器,可互相发送数据) |
| 连接类型 | 短连接(请求响应后,连接立即断开) | 长连接(建立后持续保持,直到主动关闭) |
| 实时性 | 差(依赖轮询,有延迟) | 强(服务器主动推送,无延迟) |
| 资源消耗 | 高(频繁发起请求,无效请求多) | 低(一次连接,持续通信) |
| 适用场景 | 普通接口请求(查询、新增、修改、删除) | 实时交互场景(聊天、通知、监控) |
| 协议标识 | URL以http://、https://开头 | URL以ws://、wss://开头(wss是加密版) |
⚠️ 重要提醒:WebSocket不是替代HTTP,而是补充HTTP------普通接口请求(如查询用户信息、提交订单)依然用HTTP,实时交互场景(如订单推送、聊天)用WebSocket,两者结合使用,才能适配所有业务场景。
三、WebSocket核心原理(简化讲解,新手能懂)
WebSocket的原理不复杂,核心是"握手建立连接→双向通信→关闭连接"三个步骤,新手无需深入研究底层TCP协议,重点掌握这三个步骤,就能理解WebSocket的工作流程:
- 握手建立连接(核心步骤):
-
客户端发起WebSocket连接请求(URL以ws://开头),请求头中会携带"Upgrade: websocket"(表示要升级为WebSocket协议);
-
服务器收到请求后,确认协议升级,返回响应头"Upgrade: websocket",表示同意建立WebSocket连接;
-
握手成功后,HTTP连接升级为WebSocket长连接,后续客户端和服务器之间的通信,不再使用HTTP协议,而是使用WebSocket协议。
- 双向通信:
-
连接建立后,客户端和服务器可随时向对方发送数据(数据格式通常为JSON);
-
服务器无需等待客户端请求,只要有数据更新,就能主动推送给客户端(这是WebSocket最核心的优势);
-
通信过程中,连接一直保持,除非客户端主动关闭连接,或服务器主动断开连接。
- 关闭连接:
-
客户端可主动关闭连接(如用户退出页面、关闭浏览器);
-
服务器可主动关闭连接(如连接超时、服务器重启);
-
关闭连接时,双方会发送关闭帧,确认连接关闭,避免资源泄露。
补充:WebSocket连接建立后,会保持心跳(默认每隔一段时间发送一次心跳包),用于检测连接是否正常,若心跳中断,说明连接异常,客户端会自动重新建立连接。
四、WebSocket核心概念(新手必记)
学习WebSocket实战前,需掌握3个核心概念,后续实战中会频繁用到,新手可直接记牢:
-
Session(会话):客户端和服务器建立WebSocket连接后,会创建一个唯一的Session对象,代表当前连接,服务器可通过Session对象向指定客户端推送数据;
-
消息推送方式:
-
点对点推送:服务器向单个客户端推送数据(如一对一聊天、个人通知);
-
广播推送:服务器向所有连接的客户端推送数据(如公告、弹幕、全局通知);
-
分组推送:服务器向指定分组的客户端推送数据(如群聊、部门通知)。
- 消息格式:WebSocket支持文本消息(String)和二进制消息(byte[]),实际开发中,最常用的是文本消息,格式为JSON(便于前后端解析)。
五、实战:WebSocket基础入门(原生Java实现,新手必练)
新手入门,建议先从原生Java实现WebSocket开始,熟悉WebSocket的核心API和工作流程,后续再学习Spring Boot整合(更简洁、更常用)。以下是原生Java实现WebSocket的完整步骤,可直接复制代码使用。
1. 导入依赖(pom.xml)
原生Java WebSocket依赖Java EE的javax.websocket包,Spring Boot项目中可直接导入以下依赖:
xml
<!-- 原生WebSocket依赖(Java EE) -->
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
<!-- Spring Web依赖(若项目未导入) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2. 编写WebSocket服务端(核心)
创建WebSocket服务端类,继承Endpoint,重写连接建立、消息接收、连接关闭、异常处理的方法,这是WebSocket服务端的核心:
java
package com.example.websocketdemo.ws;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
/**
* 原生Java WebSocket服务端
* ServerEndpoint:指定WebSocket的访问路径(客户端通过该路径连接)
*/
@ServerEndpoint("/ws/native")
public class NativeWebSocketServer {
// 存储所有连接的Session(key:Session ID,value:Session对象)
// ConcurrentHashMap:线程安全,适合高并发场景
private static final ConcurrentHashMap<String, Session> SESSION_MAP = new ConcurrentHashMap<>();
/**
* 连接建立成功时触发(客户端连接服务器后,自动调用)
* @param session 当前连接的Session对象
*/
@OnOpen
public void onOpen(Session session) {
// 将当前Session加入集合,便于后续推送数据
SESSION_MAP.put(session.getId(), session);
System.out.println("WebSocket连接建立成功!Session ID:" + session.getId());
// 连接成功后,向客户端推送欢迎消息
sendMessage(session, "欢迎连接WebSocket服务端,当前在线人数:" + SESSION_MAP.size());
}
/**
* 接收客户端发送的消息时触发
* @param session 当前连接的Session
* @param message 客户端发送的消息
*/
@OnMessage
public void onMessage(Session session, String message) {
System.out.println("收到客户端[" + session.getId() + "]的消息:" + message);
// 示例1:点对点推送(向发送消息的客户端回复消息)
sendMessage(session, "服务端已收到你的消息:" + message);
// 示例2:广播推送(向所有连接的客户端推送消息)
// broadcastMessage("客户端[" + session.getId() + "]发送消息:" + message);
}
/**
* 连接关闭时触发(客户端关闭连接、浏览器关闭,自动调用)
* @param session 当前连接的Session
*/
@OnClose
public void onClose(Session session) {
// 将当前Session从集合中移除,释放资源
SESSION_MAP.remove(session.getId());
System.out.println("WebSocket连接关闭!Session ID:" + session.getId() + ",当前在线人数:" + SESSION_MAP.size());
// 广播通知所有客户端,该用户已下线
broadcastMessage("客户端[" + session.getId() + "]已下线,当前在线人数:" + SESSION_MAP.size());
}
/**
* 连接异常时触发(如网络中断、服务器异常)
* @param session 当前连接的Session
* @param throwable 异常信息
*/
@OnError
public void onError(Session session, Throwable throwable) {
System.out.println("WebSocket连接异常!Session ID:" + session.getId());
throwable.printStackTrace();
// 异常时,关闭连接并移除Session
try {
session.close();
} catch (IOException e) {
e.printStackTrace();
}
SESSION_MAP.remove(session.getId());
}
/**
* 点对点推送消息(向指定客户端发送消息)
* @param session 目标客户端的Session
* @param message 要发送的消息
*/
private void sendMessage(Session session, String message) {
try {
// 判断Session是否打开(连接是否正常)
if (session.isOpen()) {
// 发送文本消息
session.getBasicRemote().sendText(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 广播推送消息(向所有连接的客户端发送消息)
* @param message 要发送的消息
*/
private void broadcastMessage(String message) {
// 遍历所有Session,向每个客户端推送消息
for (Session session : SESSION_MAP.values()) {
sendMessage(session, message);
}
}
}
3. 编写WebSocket客户端(HTML+JS,用于测试)
创建一个HTML页面,使用JavaScript原生WebSocket API连接服务端,发送消息、接收消息,用于测试WebSocket服务端是否正常工作:
html
<!DOCTYPE html>
原生WebSocket客户端原生WebSocket客户端测试
4. 测试验证(核心步骤)
-
启动Spring Boot项目,确保项目无报错;
-
将上述HTML页面保存为"index.html",放在项目的"src/main/resources/static"目录下(Spring Boot默认静态资源目录);
-
打开浏览器,访问地址:http://localhost:8080/index.html,此时会自动建立WebSocket连接;
-
在输入框中输入消息(如"Hello WebSocket"),点击"发送消息",可看到服务端回复的消息,控制台也会打印对应日志;
-
打开多个浏览器窗口(或多个标签页),访问同一个地址,发送消息,可测试广播推送(取消服务端onMessage方法中广播推送的注释);
-
点击"关闭连接",或关闭浏览器窗口,可看到服务端打印连接关闭日志,其他客户端会收到"用户下线"的广播通知。
5. 原生实现注意事项(新手避坑)
-
服务端@ServerEndpoint注解的路径,必须和客户端WebSocket连接的URL一致,否则无法建立连接;
-
存储Session的集合,必须使用线程安全的集合(如ConcurrentHashMap),因为WebSocket是多线程的,多个客户端连接会触发多个线程;
-
发送消息前,必须判断Session是否打开(session.isOpen()),避免连接已关闭时发送消息,导致异常;
-
异常处理必须完善,连接异常时要关闭Session、移除Session,避免资源泄露;
-
原生WebSocket代码繁琐,适合学习原理,实际开发中,优先使用Spring Boot整合WebSocket(更简洁、更易维护)。
第二章:实战进阶------Spring Boot整合WebSocket(实际开发首选)
掌握了原生Java WebSocket的实现后,我发现原生代码过于繁琐------需要手动管理Session、处理线程安全、编写推送逻辑,实际开发中效率太低。李哥告诉我:"Spring Boot提供了对WebSocket的自动配置,整合后代码更简洁,无需手动管理Session,还支持更多高级功能(如分组推送、消息拦截),是实际开发的首选。"
以下是Spring Boot 3.2.x整合WebSocket的完整实战步骤,涵盖基础配置、核心代码、多场景推送、测试验证,可直接复制到项目中使用,贴合实际开发场景。
一、Spring Boot整合WebSocket(基础配置)
1. 导入Maven依赖(pom.xml)
Spring Boot提供了专门的WebSocket依赖,无需导入原生Java WebSocket依赖,直接导入以下依赖即可:
xml
<!-- Spring Boot WebSocket依赖(Spring Boot 3.2.x 适配) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- Spring Web依赖(若项目未导入) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok简化代码(可选) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- fastjson(用于JSON格式转换,可选) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.32</version>
</dependency>
2. 编写WebSocket配置类(核心)
创建WebSocket配置类,开启WebSocket支持,注册WebSocket服务端,配置消息拦截器(可选),这是Spring Boot整合WebSocket的核心配置:
java
package com.example.websocketdemo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import com.example.websocketdemo.ws.SpringWebSocketHandler;
import com.example.websocketdemo.ws.WebSocketInterceptor;
/**
* Spring Boot WebSocket配置类
* EnableWebSocket:开启WebSocket支持
*/
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
/**
* 注册WebSocket服务端
* @param registry WebSocket处理器注册器
*/
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// 1. 注册WebSocket处理器(核心业务逻辑)
// 2. 指定WebSocket访问路径(/ws/spring)
// 3. addInterceptors:添加消息拦截器(可选,用于权限校验、参数解析)
// 4. setAllowedOrigins("*"):允许所有跨域请求(开发环境,生产环境需指定具体域名)
registry.addHandler(springWebSocketHandler(), "/ws/spring")
.addInterceptors(webSocketInterceptor())
.setAllowedOrigins("*");
}
/**
* 注入WebSocket处理器(核心业务逻辑)
*/
@Bean
public SpringWebSocketHandler springWebSocketHandler() {
return new SpringWebSocketHandler();
}
/**
* 注入WebSocket消息拦截器(可选)
*/
@Bean
public WebSocketInterceptor webSocketInterceptor() {
return new WebSocketInterceptor();
}
}
3. 编写WebSocket消息拦截器(可选,权限校验)
实际开发中,WebSocket连接需要权限校验(如用户登录后才能连接),可通过消息拦截器实现,在连接建立前校验用户身份,避免未授权用户连接:
java
package com.example.websocketdemo.ws;
import jakarta.servlet.http.HttpSession;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.Map;
/**
* WebSocket消息拦截器(用于权限校验、参数解析)
*/
public class WebSocketInterceptor implements HandshakeInterceptor {
/**
* 握手前触发(连接建立前,用于权限校验)
* @param request 客户端请求
* @param response 服务器响应
* @param wsHandler WebSocket处理器
* @param attributes 用于存储参数,可传递到WebSocket处理器
* @return true:允许握手(连接建立);false:拒绝握手(连接失败)
*/
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
// 1. 解析客户端请求中的参数(如用户ID、Token)
// 示例:从请求参数中获取用户ID(客户端连接时携带参数:ws://localhost:8080/ws/spring?userId=1)
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
String userId = servletRequest.getServletRequest().getParameter("userId");
String token = servletRequest.getServletRequest().getParameter("token");
// 2. 权限校验(实际开发中,需校验Token是否有效、用户是否存在)
if (userId == null || token == null || token.isEmpty()) {
// 未携带用户ID或Token,拒绝连接
System.out.println("WebSocket连接失败:未授权,缺少用户ID或Token");
return false;
}
// 3. 校验Token有效性(模拟逻辑,实际开发中替换为真实Token校验)
if (!"test_token_123".equals(token)) {
System.out.println("WebSocket连接失败:Token无效");
return false;
}
// 4. 将用户ID存入attributes,便于后续WebSocket处理器使用(点对点推送时需要)
attributes.put("userId", userId);
System.out.println("WebSocket握手成功,用户ID:" + userId);
return true;
}
/**
* 握手后触发(连接建立后,可做一些后续处理)
*/
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
// 无需处理,可留空
}
}
4. 编写WebSocket处理器(核心业务逻辑)
Spring Boot通过WebSocketHandler处理WebSocket的核心业务逻辑(连接建立、消息接收、连接关闭、异常处理),无需手动管理Session,Spring会自动维护Session,代码更简洁:
java
package com.example.websocketdemo.ws;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
/**
* Spring Boot WebSocket处理器(核心业务逻辑)
* 继承TextWebSocketHandler,专注处理文本消息(最常用)
*/
@Component
@Slf4j // 日志注解(Lombok)
public class SpringWebSocketHandler extends TextWebSocketHandler {
// 存储所有连接的Session(key:用户ID,value:WebSocketSession)
// 实际开发中,用户ID唯一,便于点对点推送(比用Session ID更直观)
private static final ConcurrentHashMap<String, WebSocketSession> USER_SESSION_MAP = new ConcurrentHashMap<>();
/**
* 连接建立成功时触发
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 从拦截器传递的attributes中获取用户ID
String userId = (String) session.getAttributes().get("userId");
if (userId != null && !userId.isEmpty()) {
// 将用户ID和Session关联,存入集合
USER_SESSION_MAP.put(userId, session);
log.info("WebSocket连接建立成功!用户ID:{},当前在线人数:{}", userId, USER_SESSION_MAP.size());
// 向当前用户推送欢迎消息(JSON格式,实际开发中常用)
JSONObject welcomeMsg = new JSONObject();
welcomeMsg.put("type", "welcome");
welcomeMsg.put("content", "欢迎连接WebSocket服务端,当前在线人数:" + USER_SESSION_MAP.size());
sendMessageToUser(userId, new TextMessage(welcomeMsg.toString()));
} else {
// 未获取到用户ID,关闭连接
session.close(CloseStatus.POLICY_VIOLATION.withReason("未获取到用户ID"));
log.error("WebSocket连接失败:未获取到用户ID");
}
}
/**
* 接收客户端发送的文本消息时触发
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 获取当前用户ID
String userId = (String) session.getAttributes().get("userId");
// 解析客户端发送的消息(JSON格式)
String messageContent = message.getPayload();
JSONObject msgJson = JSONObject.parseObject(messageContent);
String msgType = msgJson.getString("type"); // 消息类型(如chat-聊天、notice-通知)
String content = msgJson.getString("content"); // 消息内容
String targetUserId = msgJson.getString("targetUserId"); // 目标用户ID(点对点推送时使用)
log.info("收到用户[{}]的消息:类型={},内容={},目标用户ID={}", userId, msgType, content, targetUserId);
// 根据消息类型,处理不同的业务逻辑
switch (msgType) {
case "chat":
// 1. 点对点聊天(向目标用户推送消息)
if (targetUserId != null && USER_SESSION_MAP.containsKey(targetUserId)) {
JSONObject chatMsg = new JSONObject();
chatMsg.put("type", "chat");
chatMsg.put("fromUserId", userId);
chatMsg.put("content", content);
chatMsg.put("time", System.currentTimeMillis());
// 向目标用户推送消息
sendMessageToUser(targetUserId, new TextMessage(chatMsg.toString()));
// 向发送方回复"发送成功"
sendMessageToUser(userId, new TextMessage("{\"type\":\"reply\",\"content\":\"消息发送成功\"}"));
} else {
// 目标用户不在线或不存在
sendMessageToUser(userId, new TextMessage("{\"type\":\"error\",\"content\":\"目标用户不在线或不存在\"}"));
}
break;
case "broadcast":
// 2. 广播消息(向所有在线用户推送消息)
JSONObject broadcastMsg = new JSONObject();
broadcastMsg.put("type", "broadcast");
broadcastMsg.put("fromUserId", userId);
broadcastMsg.put("content", content);
broadcastMsg.put("time", System.currentTimeMillis());
broadcastMessage(new TextMessage(broadcastMsg.toString()));
break;
default:
// 未知消息类型,回复错误
sendMessageToUser(userId, new TextMessage("{\"type\":\"error\",\"content\":\"未知消息类型\"}"));
break;
}
}
/**
* 连接关闭时触发
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
// 获取当前用户ID
String userId = (String) session.getAttributes().get("userId");
if (userId != null && USER_SESSION_MAP.containsKey(userId)) {
// 从集合中移除Session,释放资源
USER_SESSION_MAP.remove(userId);
log.info("WebSocket连接关闭!用户ID:{},当前在线人数:{}", userId, USER_SESSION_MAP.size());
// 广播通知所有用户,该用户已下线
JSONObject offlineMsg = new JSONObject();
offlineMsg.put("type", "offline");
offlineMsg.put("content", "用户[" + userId + "]已下线,当前在线人数:" + USER_SESSION_MAP.size());
broadcastMessage(new TextMessage(offlineMsg.toString()));
}
}
/**
* 连接异常时触发
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
// 获取当前用户ID
String userId = (String) session.getAttributes().get("userId");
log.error("WebSocket连接异常!用户ID:{}", userId, exception);
// 异常时,关闭连接并移除Session
if (session.isOpen()) {
session.close(CloseStatus.SERVER_ERROR.withReason("连接异常"));
}
if (userId != null && USER_SESSION_MAP.containsKey(userId)) {
USER_SESSION_MAP.remove(userId);
}
}
/**
* 点对点推送消息(向指定用户发送消息)
* @param userId 目标用户ID
* @param message 要发送的消息(TextMessage:文本消息)
*/
public void sendMessageToUser(String userId, TextMessage message) {
try {
// 获取目标用户的Session
WebSocketSession session = USER_SESSION_MAP.get(userId);
// 判断Session是否打开(连接是否正常)
if (session != null && session.isOpen()) {
session.sendMessage(message);
} else {
log.warn("向用户[{}]推送消息失败:用户不在线或连接已关闭", userId);
}
} catch (IOException e) {
log.error("向用户[{}]推送消息异常", userId, e);
}
}
/**
* 广播推送消息(向所有在线用户发送消息)
* @param message 要发送的消息
*/
public void broadcastMessage(TextMessage message) {
try {
// 遍历所有在线用户的Session,推送消息
for (WebSocketSession session : USER_SESSION_MAP.values()) {
if (session.isOpen()) {
session.sendMessage(message);
}
}
} catch (IOException e) {
log.error("广播消息异常", e);
}
}
/**
* 分组推送消息(向指定分组的用户发送消息,示例:按部门分组)
* @param groupId 分组ID(如部门ID)
* @param message 要发送的消息
* @param userIds 分组内的用户ID列表
*/
public void sendMessageToGroup(String groupId, TextMessage message, String... userIds) {
try {
for (String userId : userIds) {
WebSocketSession session = USER_SESSION_MAP.get(userId);
if (session != null && session.isOpen()) {
session.sendMessage(message);
log.info("向分组[{}]的用户[{}]推送消息成功", groupId, userId);
}
}
} catch (IOException e) {
log.error("向分组[{}]推送消息异常", groupId, e);
}
}
}
5. 编写WebSocket客户端(HTML+JS,适配Spring Boot)
修改客户端代码,适配Spring Boot的WebSocket连接(携带用户ID和Token,用于权限校验),支持点对点聊天、广播消息,贴合实际开发场景:
html
<!DOCTYPE html>
Spring Boot WebSocket客户端当前用户ID:Token:
6. 测试验证(实战场景模拟)
完成上述配置和代码编写后,启动Spring Boot项目,进行多场景测试,确保WebSocket功能正常,贴合实际开发场景:
6.1 权限校验测试
-
不输入用户ID或Token,点击"建立连接",会提示"请输入用户ID和Token";
-
输入用户ID=1,Token=错误值(如123),点击"建立连接",控制台会打印"Token无效",客户端会提示"连接异常";
-
输入用户ID=1,Token=test_token_123,点击"建立连接",连接成功,客户端会收到欢迎消息。
6.2 点对点聊天测试
-
打开两个浏览器窗口(或标签页),访问同一个地址;
-
窗口1:用户ID=1,Token=test_token_123,建立连接;
-
窗口2:用户ID=2,Token=test_token_123,建立连接;
-
窗口1中,目标用户ID输入2,消息输入"你好,用户2",点击"点对点发送";
-
窗口2会收到用户1发送的消息,窗口1会收到"消息发送成功"的回复,控制台会打印对应日志。
6.3 广播消息测试
-
打开3个浏览器窗口,分别以用户ID=1、2、3建立连接;
-
用户1输入消息"大家好,这是广播消息",点击"广播发送";
-
所有窗口(用户1、2、3)都会收到这条广播消息,控制台会打印广播日志。
6.4 连接关闭测试
-
用户1建立连接后,点击"关闭连接",或关闭浏览器窗口;
-
其他用户(2、3)会收到"用户[1]已下线"的系统通知;
-
控制台会打印"用户1连接关闭"的日志,在线人数会减少1。
7. Spring Boot整合注意事项(新手避坑)
-
配置类必须添加@EnableWebSocket注解,否则无法开启WebSocket支持;
-
WebSocketHandler必须注入Spring容器(添加@Component注解),否则配置类无法获取到;
-
跨域配置:开发环境可设置setAllowedOrigins("*"),生产环境需指定具体域名(如setAllowedOrigins("https://xxx.com")),避免跨域问题;需注意域名拼写规范,避免出现多余符号(如引号残留),否则可能导致网页解析失败;
-
Session存储:实际开发中,建议用用户ID作为key存储Session(比Session ID更直观),便于点对点推送;同时需确保Session操作线程安全,优先使用ConcurrentHashMap,避免多线程并发修改导致的异常;
-
消息格式:优先使用JSON格式,便于前后端解析,可通过fastjson或Jackson进行JSON转换;避免使用复杂二进制格式,降低前后端联调成本;
-
连接异常处理:必须完善handleTransportError方法,连接异常时及时关闭Session、移除Session,避免资源泄露;同时可添加重连机制,提升客户端体验;
-
权限校验:拦截器中需严格校验用户身份(如Token有效性、用户合法性),避免未授权用户建立WebSocket连接,造成服务器资源浪费;
-
访问路径规范:WebSocket访问路径(如/ws/spring)需与客户端连接URL严格一致,避免URL拼写错误(如多写、少写字符),否则会出现"URL拼写可能存在错误,请检查"的报错;
-
依赖版本适配:Spring Boot 3.x需搭配2.0+版本的spring-boot-starter-websocket依赖,版本不匹配会导致项目启动失败或WebSocket功能异常;
-
客户端资源部署:HTML客户端页面需放在Spring Boot默认静态资源目录(src/main/resources/static),否则访问时可能出现网页解析失败、不支持的网页类型等报错;
-
心跳机制配置:生产环境需手动配置WebSocket心跳机制,避免因网络波动、连接超时导致的长连接断开,可通过定时发送心跳包检测连接状态;
-
日志打印:关键业务节点(连接建立、消息发送、异常触发)需添加详细日志,便于后续故障排查,建议使用@Slf4j注解简化日志编写;
-
生产环境优化:高并发场景下,可引入WebSocket集群部署,配合Redis实现Session共享,避免单节点压力过大,同时防止用户连接丢失。