一、WebSocket 底层基础原理1.1 HTTP 轮询方案缺陷对比突出 WebSocket 优势传统实时通信两种实现方式均存在无法根治的短板短轮询前端 setInterval 定时请求接口客户端每隔固定时间发起 HTTP 请求无论有无消息都建立连接每次请求携带完整 HTTP 请求头、Cookie带宽消耗巨大消息延迟 轮询间隔实时性差服务端 QPS 随在线人数线性暴涨。长轮询服务端 hold 住请求有消息再返回服务端阻塞 Servlet 线程无消息时挂起连接一旦有消息立刻响应前端收到后马上发起下一次长轮询缺点大量空闲连接占用 Tomcat 线程池高并发下线程耗尽服务卡死网络抖动会频繁断连重建。1.2 WebSocket 协议标准与握手流程WebSocket 是基于 TCP 的应用层协议遵循 RFC6455 规范Java 统一标准为 JSR-356Tomcat 7.0、Undertow、Jetty 全部原生支持。握手完整流程一次 HTTP 升级后续全双工客户端发起标准 GET 请求携带升级标识头GET /ws/connect/{userId} HTTP/1.1Host: 127.0.0.1:8080Upgrade: websocketConnection: UpgradeSec-WebSocket-Key: 随机Base64字符串Sec-WebSocket-Version: 13服务端校验请求头合法将 Sec-WebSocket-Key 拼接固定字符串后做 SHA1 加密返回 101 切换协议响应HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: 加密后的校验串状态码 101 代表协议升级成功TCP 连接不会断开永久复用进入 WebSocket 帧通信阶段。1.3 WebSocket 帧结构为什么比 HTTP 省流量握手完成后所有数据不再携带 HTTP 头仅传输二进制/文本帧帧头部最小仅 2 字节分为文本帧、二进制帧、关闭帧、PING/PONG 控制帧支持分片传输大文件、掩码加密浏览器客户端强制掩码。1.4 全双工核心定义全双工客户端、服务端可任意时刻主动发送数据互不阻塞HTTP 是半双工必须客户端先请求服务端才能响应服务端无法主动推送。二、JSR356 原生 WebSocket 生命周期全注解详解SpringBoot 中 ServerEndpoint 是原生标准注解不由 Spring 管理由 Web 容器Tomcat实例化五大生命周期回调完整说明2.1 ServerEndpoint(value “/ws/connect/{uid}”)作用声明 WebSocket 接入端点路径支持路径变量 {uid}特性每一个客户端连接都会新建一个该类实例每个连接独享对象类中成员变量不共享限制无法直接使用 Autowired、Resource 注入 Spring Bean后文给出通用解决方案。2.2 OnOpen 连接建立回调触发时机HTTP 握手成功、协议升级完成TCP 长连接建立完毕。可获取参数Session当前连接会话核心对象所有消息发送、关闭操作依赖它PathParam(“uid”) Long uid获取路径上的用户唯一标识业务通用执行逻辑Token 鉴权、在线会话存入内存Map、用户上线触发离线消息补发。2.3 OnMessage 消息接收回调两种重载onMessage(String msg)接收前端文本帧聊天、心跳JSON通用onMessage(byte[] bytes)接收二进制帧图片、文件流。通用处理场景解析 PING 心跳包返回 PONG解析业务JSON消息调用业务层处理过滤空消息、非法格式消息。2.4 OnClose 连接关闭回调触发时机分为两类主动关闭前端调用 websocket.close()、服务端执行 session.close()正常被动关闭页面关闭、浏览器标签销毁。入参CloseReason 可获取关闭状态码、关闭描述文案。通用逻辑从在线Map移除当前用户Session更新在线状态。2.5 OnError 连接异常回调触发场景网络断开、IO 异常、并发写通道冲突、前端强制断网。特殊区分EOFException 属于正常通道关闭无需打印异常堆栈其余网络、并发异常需要记录错误日志。通用逻辑清理失效Session防止僵尸连接残留。三、生产环境七大核心通用技术方案无业务耦合3.1 方案一握手阶段URL携带Token鉴权通用安全标准3.1.1 原生WebSocket鉴权痛点JSR356 没有 SpringMVC 拦截器、过滤器上下文无法从 Header 中直接读取Token浏览器JS WebSocket不支持自定义请求头主流两种兼容方案URL Query 参数携带 Token兼容性最好所有前端框架通用先HTTP登录获取Ticket通过路径传递短凭证。3.1.2 完整鉴权执行流程前端连接地址拼接Tokenws://127.0.0.1:8080/ws/connect/10001?tokeneyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9OnOpen 内解析Query字符串提取token值通用JWT校验逻辑读取密钥、解析Claims、校验过期时间、校验签名校验失败调用 session.close(CloseCodes.CANNOT_ACCEPT, “Token无效”) 主动断开拒绝非法连接校验通过存入在线会话Map允许后续消息收发。3.1.3 URL参数解析通用工具方法细节兼容Token内部包含 、 等特殊字符使用 URLDecoder.decode 解码解决前端URL编码后参数乱码判空拦截空Token、空白Token杜绝匿名连接。3.2 方案二在线会话线程安全管理 多端互踢机制3.2.1 存储容器选型ConcurrentHashMap不能使用普通 HashMap上线、下线、多线程推送会并发读写触发 ConcurrentModificationException。容器定义标准// Key全局唯一用户IDValue当前用户有效WebSocket会话private static final MapLong, Session ONLINE_SESSION_MAP new ConcurrentHashMap();3.2.2 多端互踢实现逻辑同一用户新设备发起连接时先执行 ONLINE_SESSION_MAP.remove(userId)如果旧Session存在自动失效新Session放入Map保证一个用户同一时间仅存在一条有效长连接适用场景账号单点登录、PC端/移动端互踢。3.2.3 Session 对象生命周期注意每个连接独立Session仅存于内存服务重启全部丢失单机部署有效集群环境失效集群改造见文末。3.3 方案三PING/PONG 心跳保活机制解决僵尸连接3.3.1 僵尸连接产生原因用户手机锁屏、浏览器后台休眠路由器/防火墙自动切断空闲TCP连接用户直接断网、关闭电脑不会触发 onClose 回调服务端Map中残留失效Session推送消息时频繁报错占用内存。3.3.2 标准心跳交互规范前端定时任务20~30s发送纯文本字符串 PING服务端 OnMessage 拦截PING消息立刻回复 PONG扩展增强方案生产推荐内存维护 MapLong, Long userLastHeartbeat记录每个用户最后心跳时间戳启动定时任务30s执行一次遍历对比当前时间超过90s无心跳则主动关闭Session、清理Map。3.3.3 心跳包设计优势轻量无序列化开销仅简单字符串双向验证连通性前端收不到PONG自动触发重连逻辑。3.4 方案四消息推送线程安全锁解决并发写入异常3.4.1 报错根源JSR356 规范明确Session 会话对象非线程安全。多个异步线程同时调用 session.getBasicRemote().sendText() 发送消息会抛出固定异常java.lang.IllegalStateException: The remote endpoint was in state [TEXT_PARTIAL_WRITING]场景举例同时给同一个用户推送系统通知、聊天消息、离线消息多线程并发写入通道冲突。3.4.2 通用标准解决方案对单个Session对象加synchronized同步锁串行执行消息发送private void safeSendText(Session session, String json) throws IOException {synchronized (session) {// 发送前必须判断连接是否开启避免关闭后写入报错if (session.isOpen()) {session.getBasicRemote().sendText(json);}}}锁粒度仅绑定单个会话不同用户之间不会互相阻塞无性能损耗。3.4.3 全局统一推送静态方法封装静态工具 sendToUser(Long uid, Object data)业务层、定时任务、离线消息统一调用统一处理空Session、连接关闭、序列化异常消除重复代码。3.5 方案五离线消息通用持久化补发模型适用于私信、IM聊天、站内通知通用不绑定任何业务表结构离线存储阶段用户不在线时消息持久化到数据库标记字段 read_status 0未读关联接收人ID、会话ID上线拉取阶段用户WebSocket握手成功后自动查询当前用户所有未读数据批量推送阶段循环遍历未读消息调用安全推送方法逐条下发前端状态更新防重阶段全部推送完成后批量UPDATE数据库将消息改为 read_status1同时清空会话未读计数核心目的用户频繁断线重连时不会重复推送历史离线消息从数据层兜底防重复。3.6 方案六数据库事务与WebSocket推送解耦数据一致性规范3.6.1 错误写法推送放入事务内Transactional(rollbackFor Exception.class)public void sendMsg(){// 1.数据库插入消息insertMsg();// 2.事务内执行WebSocket推送ChatWebSocketServer.sendToUser(uid, dto);}缺陷WebSocket推送是IO阻塞操作网络延迟会长期占用数据库连接推送抛出异常会触发事务回滚导致消息丢失数据不一致数据库连接池快速耗尽高并发服务卡死。3.6.2 标准正确分层写法public void saveAndPush(Long senderId, MsgDTO dto) {// 1.事务方法仅做数据库持久化无任何推送逻辑saveInTransaction(senderId, dto);// 2.事务提交完成后外部执行推送推送失败不影响数据try {ChatWebSocketServer.sendToUser(dto.getReceiverId(), dto);} catch (Exception e) {log.error(“消息推送失败可前端轮询兜底拉取”, e);}}Transactional(rollbackFor Exception.class)public void saveInTransaction(Long senderId, MsgDTO dto) {// 插入消息、更新会话、未读数自增}规范总结事务只负责数据落库推送作为容错旁路即使推送失败前端可通过HTTP接口轮询加载历史消息兜底。3.7 方案七WebSocket 获取Spring Bean通用解决方案3.7.1 问题底层原因ServerEndpoint 标注的类由 Tomcat Web容器实例化不在Spring IOC容器管理范围内构造函数、成员变量无法使用 Autowired 注入Service、Mapper。3.7.2 通用无框架依赖方案通过Spring上下文工具类静态获取Bean标准工具逻辑import org.springframework.context.ApplicationContext;import org.springframework.core.env.Environment;public class SpringContextUtil {private static ApplicationContext context;public static void setContext(ApplicationContext applicationContext) { context applicationContext; } // 根据Class获取Bean public static T T getBean(ClassT clazz) { return context.getBean(clazz); } // 获取配置文件环境变量如JWT密钥 public static Environment getEnv() { return context.getBean(Environment.class); }}在项目启动类 main 方法注入上下文SpringBootApplicationpublic class App {public static void main(String[] args) {ApplicationContext context SpringApplication.run(App.class, args);SpringContextUtil.setContext(context);}}WebSocket中使用// 静态获取业务Service无注入依赖private static MsgService getMsgService() {return SpringContextUtil.getBean(MsgService.class);}四、通用生产级完整WebSocket模板无业务、可直接复制import cn.hutool.core.util.StrUtil;import cn.hutool.json.JSONUtil;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;import javax.websocket.*;import javax.websocket.server.PathParam;import javax.websocket.server.ServerEndpoint;import java.io.IOException;import java.net.URLDecoder;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;/**通用生产级WebSocket模板内置URL Token鉴权、多端互踢、PING/PONG心跳、线程安全推送、异常清理完全剥离业务逻辑适配所有SpringBoot项目*/Slf4jComponentServerEndpoint(“/ws/connect/{userId}”)public class StandardWebSocketServer {// 在线用户会话池线程安全private static final MapLong, Session ONLINE_SESSION_MAP new ConcurrentHashMap();/**连接建立鉴权、注册会话、上线补发离线消息*/OnOpenpublic void onOpen(Session session, PathParam(“userId”) Long userId) {// 1. 解析URL中的token参数String token getQueryParam(session, “token”);if (StrUtil.isBlank(token)) {closeSession(session, CloseReason.CloseCodes.CANNOT_ACCEPT, “连接失败未携带登录凭证”);return;}// 2. 通用JWT校验可替换任意项目校验工具boolean tokenValid verifyJwtToken(token);if (!tokenValid) {closeSession(session, CloseReason.CloseCodes.CANNOT_ACCEPT, “连接失败Token过期或非法”);return;}// 3. 多端互踢移除旧连接存入新会话ONLINE_SESSION_MAP.remove(userId);ONLINE_SESSION_MAP.put(userId, session);log.info(“用户{} WebSocket连接建立成功”, userId);// 4. 上线触发离线消息补发通过静态工具获取业务Servicetry {MsgService msgService SpringContextUtil.getBean(MsgService.class);msgService.pushOfflineMsg(userId);} catch (Exception e) {log.error(“用户{} 离线消息推送异常”, userId, e);}}/**接收客户端消息区分心跳、业务消息*/OnMessagepublic void onMessage(String content, PathParam(“userId”) Long userId) {if (StrUtil.isBlank(content)) {return;}// 处理PING心跳回复PONG保活if (“PING”.equals(content)) {Session session ONLINE_SESSION_MAP.get(userId);try {safeSendText(session, “PONG”);} catch (IOException e) {log.error(“向用户{} 回复心跳PONG失败”, userId, e);}return;}// 忽略前端收到的PONG响应if (“PONG”.equals(content)) {return;}// 处理业务JSON消息try {MsgDTO dto JSONUtil.toBean(content, MsgDTO.class);MsgService service SpringContextUtil.getBean(MsgService.class);service.saveAndSendMsg(userId, dto);} catch (Exception e) {log.error(“用户{} 业务消息解析处理异常”, userId, e);}}/**连接关闭回调清理在线会话*/OnClosepublic void onClose(PathParam(“userId”) Long userId, CloseReason reason) {ONLINE_SESSION_MAP.remove(userId);log.info(“用户{} 连接关闭关闭码{}”, userId, reason.getCloseCode());}/**连接异常回调区分正常关闭与异常崩溃*/OnErrorpublic void onError(Session session, Throwable throwable, PathParam(“userId”) Long userId) {// EOFException属于正常通道关闭不打印堆栈if (!(throwable instanceof java.io.EOFException)) {log.error(“用户{} WebSocket连接异常”, userId, throwable);}ONLINE_SESSION_MAP.remove(userId);}// 通用工具方法 /**对外静态推送入口供业务层全局调用*/public static void sendToUser(Long userId, Object data) {Session session ONLINE_SESSION_MAP.get(userId);if (session null || !session.isOpen()) {return;}try {String json JSONUtil.toJsonStr(data);safeSendText(session, json);} catch (IOException e) {log.error(“向用户{} 推送消息失败”, userId, e);}}/**线程安全发送文本单会话同步锁解决并发写入冲突*/private static void safeSendText(Session session, String text) throws IOException {synchronized (session) {if (session.isOpen()) {session.getBasicRemote().sendText(text);}}}/**统一关闭会话封装消除重复try-catch*/private void closeSession(Session session, CloseReason.CloseCodes code, String desc) {try {session.close(new CloseReason(code, desc));} catch (Exception e) {log.error(“主动关闭会话异常”, e);}}/**解析URL Query参数兼容带、的Token自动URL解码*/private String getQueryParam(Session session, String paramKey) {String queryStr session.getQueryString();if (StrUtil.isBlank(queryStr)) {return null;}String[] paramArr queryStr.split(“”);for (String item : paramArr) {int eqIndex item.indexOf(“”);if (eqIndex -1) {continue;}String key item.substring(0, eqIndex);String value item.substring(eqIndex 1);if (paramKey.equals(key)) {try {return URLDecoder.decode(value, “UTF-8”);} catch (Exception e) {log.error(“URL参数解码失败”, e);return value;}}}return null;}/**通用JWT校验方法可替换任意项目工具类*/private boolean verifyJwtToken(String token) {try {// 读取配置文件密钥、解析签名、校验过期时间String secret SpringContextUtil.getEnv().getProperty(“token.secret”);// JWT解析逻辑省略return true;} catch (Exception e) {return false;}}}五、线上生产高频坑点深度拆解原理解决方案坑1并发推送报 TEXT_PARTIAL_WRITING底层原理Session底层输出流未写完时其他线程再次写入帧数据错乱根治方案发送方法加 synchronized (session) 锁串行发送避坑误区加全局锁锁类会导致所有用户消息排队性能暴跌。坑2WebSocket无法注入Service/Mapper原理容器托管实例脱离Spring IOC生命周期方案静态上下文工具类动态获取Bean无任何侵入拓展不建议使用静态变量持有Service会引发单例线程安全问题。坑3用户断网后服务端仍标记在线推送全失败原因静默断网不会触发onClose内存Map残留僵尸Session双层解决方案前端PING/PONG心跳维持活性后台定时任务清理长时间无心跳连接主动关闭失效Session。坑4用户频繁重连离线消息重复推送根源推送完未更新数据库消息已读状态标准流程推送全部离线消息后批量UPDATE修改读取状态从数据层杜绝重复下发。坑5裸WebSocket接口无鉴权安全漏洞风险任意客户端拼接ws地址即可建立连接接收全部推送消息强制规范握手阶段必须校验Token非法连接直接close禁止匿名接入。坑6推送逻辑写在Transactional事务内数据库连接耗尽底层IO阻塞占用连接事务超时回滚导致消息丢失分层规范事务只做CRUD推送逻辑放在事务外部捕获异常前端轮询兜底。坑7集群部署后不同服务实例用户无法互通消息单机局限Session存储在本机内存其他服务节点无法访问分布式改造通用方案Redis发布订阅模式A服务收到消息发布Msg至Redis Topic所有服务节点订阅Topic匹配接收用户本地Session并推送实现跨实例全集群消息互通。六、线上性能与安全扩展优化技术点6.1 性能优化消息序列化优化统一使用Hutool JSON序列化避免FastJSON漏洞高频消息可改用Protobuf二进制传输连接限流新增拦截器限制单IP、单账号最大并发连接数防御DOS攻击异步推送线程池单独创建线程池处理消息推送不占用Tomcat业务线程消息缓存高频会话历史消息存入Redis减少MySQL分页查询压力。6.2 安全优化wss加密协议生产环境强制使用wss://WebSocketSSL明文ws禁止外网访问Token短期有效期JWT设置短过期时间前端定时刷新Token重连携带新凭证消息内容过滤增加敏感词拦截、XSS过滤防止前端注入恶意脚本关闭超时连接限制单条连接空闲最大时长自动回收资源。
SpringBoot WebSocket 深度技术全解(纯通用无业务、生产落地完整版)
一、WebSocket 底层基础原理1.1 HTTP 轮询方案缺陷对比突出 WebSocket 优势传统实时通信两种实现方式均存在无法根治的短板短轮询前端 setInterval 定时请求接口客户端每隔固定时间发起 HTTP 请求无论有无消息都建立连接每次请求携带完整 HTTP 请求头、Cookie带宽消耗巨大消息延迟 轮询间隔实时性差服务端 QPS 随在线人数线性暴涨。长轮询服务端 hold 住请求有消息再返回服务端阻塞 Servlet 线程无消息时挂起连接一旦有消息立刻响应前端收到后马上发起下一次长轮询缺点大量空闲连接占用 Tomcat 线程池高并发下线程耗尽服务卡死网络抖动会频繁断连重建。1.2 WebSocket 协议标准与握手流程WebSocket 是基于 TCP 的应用层协议遵循 RFC6455 规范Java 统一标准为 JSR-356Tomcat 7.0、Undertow、Jetty 全部原生支持。握手完整流程一次 HTTP 升级后续全双工客户端发起标准 GET 请求携带升级标识头GET /ws/connect/{userId} HTTP/1.1Host: 127.0.0.1:8080Upgrade: websocketConnection: UpgradeSec-WebSocket-Key: 随机Base64字符串Sec-WebSocket-Version: 13服务端校验请求头合法将 Sec-WebSocket-Key 拼接固定字符串后做 SHA1 加密返回 101 切换协议响应HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: 加密后的校验串状态码 101 代表协议升级成功TCP 连接不会断开永久复用进入 WebSocket 帧通信阶段。1.3 WebSocket 帧结构为什么比 HTTP 省流量握手完成后所有数据不再携带 HTTP 头仅传输二进制/文本帧帧头部最小仅 2 字节分为文本帧、二进制帧、关闭帧、PING/PONG 控制帧支持分片传输大文件、掩码加密浏览器客户端强制掩码。1.4 全双工核心定义全双工客户端、服务端可任意时刻主动发送数据互不阻塞HTTP 是半双工必须客户端先请求服务端才能响应服务端无法主动推送。二、JSR356 原生 WebSocket 生命周期全注解详解SpringBoot 中 ServerEndpoint 是原生标准注解不由 Spring 管理由 Web 容器Tomcat实例化五大生命周期回调完整说明2.1 ServerEndpoint(value “/ws/connect/{uid}”)作用声明 WebSocket 接入端点路径支持路径变量 {uid}特性每一个客户端连接都会新建一个该类实例每个连接独享对象类中成员变量不共享限制无法直接使用 Autowired、Resource 注入 Spring Bean后文给出通用解决方案。2.2 OnOpen 连接建立回调触发时机HTTP 握手成功、协议升级完成TCP 长连接建立完毕。可获取参数Session当前连接会话核心对象所有消息发送、关闭操作依赖它PathParam(“uid”) Long uid获取路径上的用户唯一标识业务通用执行逻辑Token 鉴权、在线会话存入内存Map、用户上线触发离线消息补发。2.3 OnMessage 消息接收回调两种重载onMessage(String msg)接收前端文本帧聊天、心跳JSON通用onMessage(byte[] bytes)接收二进制帧图片、文件流。通用处理场景解析 PING 心跳包返回 PONG解析业务JSON消息调用业务层处理过滤空消息、非法格式消息。2.4 OnClose 连接关闭回调触发时机分为两类主动关闭前端调用 websocket.close()、服务端执行 session.close()正常被动关闭页面关闭、浏览器标签销毁。入参CloseReason 可获取关闭状态码、关闭描述文案。通用逻辑从在线Map移除当前用户Session更新在线状态。2.5 OnError 连接异常回调触发场景网络断开、IO 异常、并发写通道冲突、前端强制断网。特殊区分EOFException 属于正常通道关闭无需打印异常堆栈其余网络、并发异常需要记录错误日志。通用逻辑清理失效Session防止僵尸连接残留。三、生产环境七大核心通用技术方案无业务耦合3.1 方案一握手阶段URL携带Token鉴权通用安全标准3.1.1 原生WebSocket鉴权痛点JSR356 没有 SpringMVC 拦截器、过滤器上下文无法从 Header 中直接读取Token浏览器JS WebSocket不支持自定义请求头主流两种兼容方案URL Query 参数携带 Token兼容性最好所有前端框架通用先HTTP登录获取Ticket通过路径传递短凭证。3.1.2 完整鉴权执行流程前端连接地址拼接Tokenws://127.0.0.1:8080/ws/connect/10001?tokeneyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9OnOpen 内解析Query字符串提取token值通用JWT校验逻辑读取密钥、解析Claims、校验过期时间、校验签名校验失败调用 session.close(CloseCodes.CANNOT_ACCEPT, “Token无效”) 主动断开拒绝非法连接校验通过存入在线会话Map允许后续消息收发。3.1.3 URL参数解析通用工具方法细节兼容Token内部包含 、 等特殊字符使用 URLDecoder.decode 解码解决前端URL编码后参数乱码判空拦截空Token、空白Token杜绝匿名连接。3.2 方案二在线会话线程安全管理 多端互踢机制3.2.1 存储容器选型ConcurrentHashMap不能使用普通 HashMap上线、下线、多线程推送会并发读写触发 ConcurrentModificationException。容器定义标准// Key全局唯一用户IDValue当前用户有效WebSocket会话private static final MapLong, Session ONLINE_SESSION_MAP new ConcurrentHashMap();3.2.2 多端互踢实现逻辑同一用户新设备发起连接时先执行 ONLINE_SESSION_MAP.remove(userId)如果旧Session存在自动失效新Session放入Map保证一个用户同一时间仅存在一条有效长连接适用场景账号单点登录、PC端/移动端互踢。3.2.3 Session 对象生命周期注意每个连接独立Session仅存于内存服务重启全部丢失单机部署有效集群环境失效集群改造见文末。3.3 方案三PING/PONG 心跳保活机制解决僵尸连接3.3.1 僵尸连接产生原因用户手机锁屏、浏览器后台休眠路由器/防火墙自动切断空闲TCP连接用户直接断网、关闭电脑不会触发 onClose 回调服务端Map中残留失效Session推送消息时频繁报错占用内存。3.3.2 标准心跳交互规范前端定时任务20~30s发送纯文本字符串 PING服务端 OnMessage 拦截PING消息立刻回复 PONG扩展增强方案生产推荐内存维护 MapLong, Long userLastHeartbeat记录每个用户最后心跳时间戳启动定时任务30s执行一次遍历对比当前时间超过90s无心跳则主动关闭Session、清理Map。3.3.3 心跳包设计优势轻量无序列化开销仅简单字符串双向验证连通性前端收不到PONG自动触发重连逻辑。3.4 方案四消息推送线程安全锁解决并发写入异常3.4.1 报错根源JSR356 规范明确Session 会话对象非线程安全。多个异步线程同时调用 session.getBasicRemote().sendText() 发送消息会抛出固定异常java.lang.IllegalStateException: The remote endpoint was in state [TEXT_PARTIAL_WRITING]场景举例同时给同一个用户推送系统通知、聊天消息、离线消息多线程并发写入通道冲突。3.4.2 通用标准解决方案对单个Session对象加synchronized同步锁串行执行消息发送private void safeSendText(Session session, String json) throws IOException {synchronized (session) {// 发送前必须判断连接是否开启避免关闭后写入报错if (session.isOpen()) {session.getBasicRemote().sendText(json);}}}锁粒度仅绑定单个会话不同用户之间不会互相阻塞无性能损耗。3.4.3 全局统一推送静态方法封装静态工具 sendToUser(Long uid, Object data)业务层、定时任务、离线消息统一调用统一处理空Session、连接关闭、序列化异常消除重复代码。3.5 方案五离线消息通用持久化补发模型适用于私信、IM聊天、站内通知通用不绑定任何业务表结构离线存储阶段用户不在线时消息持久化到数据库标记字段 read_status 0未读关联接收人ID、会话ID上线拉取阶段用户WebSocket握手成功后自动查询当前用户所有未读数据批量推送阶段循环遍历未读消息调用安全推送方法逐条下发前端状态更新防重阶段全部推送完成后批量UPDATE数据库将消息改为 read_status1同时清空会话未读计数核心目的用户频繁断线重连时不会重复推送历史离线消息从数据层兜底防重复。3.6 方案六数据库事务与WebSocket推送解耦数据一致性规范3.6.1 错误写法推送放入事务内Transactional(rollbackFor Exception.class)public void sendMsg(){// 1.数据库插入消息insertMsg();// 2.事务内执行WebSocket推送ChatWebSocketServer.sendToUser(uid, dto);}缺陷WebSocket推送是IO阻塞操作网络延迟会长期占用数据库连接推送抛出异常会触发事务回滚导致消息丢失数据不一致数据库连接池快速耗尽高并发服务卡死。3.6.2 标准正确分层写法public void saveAndPush(Long senderId, MsgDTO dto) {// 1.事务方法仅做数据库持久化无任何推送逻辑saveInTransaction(senderId, dto);// 2.事务提交完成后外部执行推送推送失败不影响数据try {ChatWebSocketServer.sendToUser(dto.getReceiverId(), dto);} catch (Exception e) {log.error(“消息推送失败可前端轮询兜底拉取”, e);}}Transactional(rollbackFor Exception.class)public void saveInTransaction(Long senderId, MsgDTO dto) {// 插入消息、更新会话、未读数自增}规范总结事务只负责数据落库推送作为容错旁路即使推送失败前端可通过HTTP接口轮询加载历史消息兜底。3.7 方案七WebSocket 获取Spring Bean通用解决方案3.7.1 问题底层原因ServerEndpoint 标注的类由 Tomcat Web容器实例化不在Spring IOC容器管理范围内构造函数、成员变量无法使用 Autowired 注入Service、Mapper。3.7.2 通用无框架依赖方案通过Spring上下文工具类静态获取Bean标准工具逻辑import org.springframework.context.ApplicationContext;import org.springframework.core.env.Environment;public class SpringContextUtil {private static ApplicationContext context;public static void setContext(ApplicationContext applicationContext) { context applicationContext; } // 根据Class获取Bean public static T T getBean(ClassT clazz) { return context.getBean(clazz); } // 获取配置文件环境变量如JWT密钥 public static Environment getEnv() { return context.getBean(Environment.class); }}在项目启动类 main 方法注入上下文SpringBootApplicationpublic class App {public static void main(String[] args) {ApplicationContext context SpringApplication.run(App.class, args);SpringContextUtil.setContext(context);}}WebSocket中使用// 静态获取业务Service无注入依赖private static MsgService getMsgService() {return SpringContextUtil.getBean(MsgService.class);}四、通用生产级完整WebSocket模板无业务、可直接复制import cn.hutool.core.util.StrUtil;import cn.hutool.json.JSONUtil;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;import javax.websocket.*;import javax.websocket.server.PathParam;import javax.websocket.server.ServerEndpoint;import java.io.IOException;import java.net.URLDecoder;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;/**通用生产级WebSocket模板内置URL Token鉴权、多端互踢、PING/PONG心跳、线程安全推送、异常清理完全剥离业务逻辑适配所有SpringBoot项目*/Slf4jComponentServerEndpoint(“/ws/connect/{userId}”)public class StandardWebSocketServer {// 在线用户会话池线程安全private static final MapLong, Session ONLINE_SESSION_MAP new ConcurrentHashMap();/**连接建立鉴权、注册会话、上线补发离线消息*/OnOpenpublic void onOpen(Session session, PathParam(“userId”) Long userId) {// 1. 解析URL中的token参数String token getQueryParam(session, “token”);if (StrUtil.isBlank(token)) {closeSession(session, CloseReason.CloseCodes.CANNOT_ACCEPT, “连接失败未携带登录凭证”);return;}// 2. 通用JWT校验可替换任意项目校验工具boolean tokenValid verifyJwtToken(token);if (!tokenValid) {closeSession(session, CloseReason.CloseCodes.CANNOT_ACCEPT, “连接失败Token过期或非法”);return;}// 3. 多端互踢移除旧连接存入新会话ONLINE_SESSION_MAP.remove(userId);ONLINE_SESSION_MAP.put(userId, session);log.info(“用户{} WebSocket连接建立成功”, userId);// 4. 上线触发离线消息补发通过静态工具获取业务Servicetry {MsgService msgService SpringContextUtil.getBean(MsgService.class);msgService.pushOfflineMsg(userId);} catch (Exception e) {log.error(“用户{} 离线消息推送异常”, userId, e);}}/**接收客户端消息区分心跳、业务消息*/OnMessagepublic void onMessage(String content, PathParam(“userId”) Long userId) {if (StrUtil.isBlank(content)) {return;}// 处理PING心跳回复PONG保活if (“PING”.equals(content)) {Session session ONLINE_SESSION_MAP.get(userId);try {safeSendText(session, “PONG”);} catch (IOException e) {log.error(“向用户{} 回复心跳PONG失败”, userId, e);}return;}// 忽略前端收到的PONG响应if (“PONG”.equals(content)) {return;}// 处理业务JSON消息try {MsgDTO dto JSONUtil.toBean(content, MsgDTO.class);MsgService service SpringContextUtil.getBean(MsgService.class);service.saveAndSendMsg(userId, dto);} catch (Exception e) {log.error(“用户{} 业务消息解析处理异常”, userId, e);}}/**连接关闭回调清理在线会话*/OnClosepublic void onClose(PathParam(“userId”) Long userId, CloseReason reason) {ONLINE_SESSION_MAP.remove(userId);log.info(“用户{} 连接关闭关闭码{}”, userId, reason.getCloseCode());}/**连接异常回调区分正常关闭与异常崩溃*/OnErrorpublic void onError(Session session, Throwable throwable, PathParam(“userId”) Long userId) {// EOFException属于正常通道关闭不打印堆栈if (!(throwable instanceof java.io.EOFException)) {log.error(“用户{} WebSocket连接异常”, userId, throwable);}ONLINE_SESSION_MAP.remove(userId);}// 通用工具方法 /**对外静态推送入口供业务层全局调用*/public static void sendToUser(Long userId, Object data) {Session session ONLINE_SESSION_MAP.get(userId);if (session null || !session.isOpen()) {return;}try {String json JSONUtil.toJsonStr(data);safeSendText(session, json);} catch (IOException e) {log.error(“向用户{} 推送消息失败”, userId, e);}}/**线程安全发送文本单会话同步锁解决并发写入冲突*/private static void safeSendText(Session session, String text) throws IOException {synchronized (session) {if (session.isOpen()) {session.getBasicRemote().sendText(text);}}}/**统一关闭会话封装消除重复try-catch*/private void closeSession(Session session, CloseReason.CloseCodes code, String desc) {try {session.close(new CloseReason(code, desc));} catch (Exception e) {log.error(“主动关闭会话异常”, e);}}/**解析URL Query参数兼容带、的Token自动URL解码*/private String getQueryParam(Session session, String paramKey) {String queryStr session.getQueryString();if (StrUtil.isBlank(queryStr)) {return null;}String[] paramArr queryStr.split(“”);for (String item : paramArr) {int eqIndex item.indexOf(“”);if (eqIndex -1) {continue;}String key item.substring(0, eqIndex);String value item.substring(eqIndex 1);if (paramKey.equals(key)) {try {return URLDecoder.decode(value, “UTF-8”);} catch (Exception e) {log.error(“URL参数解码失败”, e);return value;}}}return null;}/**通用JWT校验方法可替换任意项目工具类*/private boolean verifyJwtToken(String token) {try {// 读取配置文件密钥、解析签名、校验过期时间String secret SpringContextUtil.getEnv().getProperty(“token.secret”);// JWT解析逻辑省略return true;} catch (Exception e) {return false;}}}五、线上生产高频坑点深度拆解原理解决方案坑1并发推送报 TEXT_PARTIAL_WRITING底层原理Session底层输出流未写完时其他线程再次写入帧数据错乱根治方案发送方法加 synchronized (session) 锁串行发送避坑误区加全局锁锁类会导致所有用户消息排队性能暴跌。坑2WebSocket无法注入Service/Mapper原理容器托管实例脱离Spring IOC生命周期方案静态上下文工具类动态获取Bean无任何侵入拓展不建议使用静态变量持有Service会引发单例线程安全问题。坑3用户断网后服务端仍标记在线推送全失败原因静默断网不会触发onClose内存Map残留僵尸Session双层解决方案前端PING/PONG心跳维持活性后台定时任务清理长时间无心跳连接主动关闭失效Session。坑4用户频繁重连离线消息重复推送根源推送完未更新数据库消息已读状态标准流程推送全部离线消息后批量UPDATE修改读取状态从数据层杜绝重复下发。坑5裸WebSocket接口无鉴权安全漏洞风险任意客户端拼接ws地址即可建立连接接收全部推送消息强制规范握手阶段必须校验Token非法连接直接close禁止匿名接入。坑6推送逻辑写在Transactional事务内数据库连接耗尽底层IO阻塞占用连接事务超时回滚导致消息丢失分层规范事务只做CRUD推送逻辑放在事务外部捕获异常前端轮询兜底。坑7集群部署后不同服务实例用户无法互通消息单机局限Session存储在本机内存其他服务节点无法访问分布式改造通用方案Redis发布订阅模式A服务收到消息发布Msg至Redis Topic所有服务节点订阅Topic匹配接收用户本地Session并推送实现跨实例全集群消息互通。六、线上性能与安全扩展优化技术点6.1 性能优化消息序列化优化统一使用Hutool JSON序列化避免FastJSON漏洞高频消息可改用Protobuf二进制传输连接限流新增拦截器限制单IP、单账号最大并发连接数防御DOS攻击异步推送线程池单独创建线程池处理消息推送不占用Tomcat业务线程消息缓存高频会话历史消息存入Redis减少MySQL分页查询压力。6.2 安全优化wss加密协议生产环境强制使用wss://WebSocketSSL明文ws禁止外网访问Token短期有效期JWT设置短过期时间前端定时刷新Token重连携带新凭证消息内容过滤增加敏感词拦截、XSS过滤防止前端注入恶意脚本关闭超时连接限制单条连接空闲最大时长自动回收资源。