news 2026/7/11 21:00:41

SpringBoot WebSocket 深度技术全解(纯通用无业务、生产落地完整版)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot WebSocket 深度技术全解(纯通用无业务、生产落地完整版)

一、WebSocket 底层基础原理
1.1 HTTP 轮询方案缺陷(对比突出 WebSocket 优势)
传统实时通信两种实现方式,均存在无法根治的短板:

短轮询(前端 setInterval 定时请求接口)

客户端每隔固定时间发起 HTTP 请求,无论有无消息都建立连接;

每次请求携带完整 HTTP 请求头、Cookie,带宽消耗巨大;

消息延迟 = 轮询间隔,实时性差;服务端 QPS 随在线人数线性暴涨。

长轮询(服务端 hold 住请求,有消息再返回)

服务端阻塞 Servlet 线程,无消息时挂起连接;

一旦有消息立刻响应,前端收到后马上发起下一次长轮询;

缺点:大量空闲连接占用 Tomcat 线程池,高并发下线程耗尽服务卡死;网络抖动会频繁断连重建。

1.2 WebSocket 协议标准与握手流程
WebSocket 是基于 TCP 的应用层协议,遵循 RFC6455 规范,Java 统一标准为 JSR-356,Tomcat 7.0+、Undertow、Jetty 全部原生支持。

握手完整流程(一次 HTTP 升级,后续全双工)
客户端发起标准 GET 请求,携带升级标识头:
GET /ws/connect/{userId} HTTP/1.1
Host: 127.0.0.1:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: 随机Base64字符串
Sec-WebSocket-Version: 13
服务端校验请求头合法,将 Sec-WebSocket-Key 拼接固定字符串后做 SHA1 加密,返回 101 切换协议响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-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 完整鉴权执行流程
前端连接地址拼接Token:ws://127.0.0.1:8080/ws/connect/10001?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

@OnOpen 内解析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:全局唯一用户ID,Value:当前用户有效WebSocket会话
private static final Map<Long, 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;

扩展增强方案(生产推荐):

内存维护 Map<Long, 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_status=1,同时清空会话未读计数;
核心目的:用户频繁断线重连时,不会重复推送历史离线消息,从数据层兜底防重复。

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(Class<T> clazz) { return context.getBean(clazz); } // 获取配置文件环境变量(如JWT密钥) public static Environment getEnv() { return context.getBean(Environment.class); }

}
在项目启动类 main 方法注入上下文:

@SpringBootApplication
public 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项目
    */
    @Slf4j
    @Component
    @ServerEndpoint(“/ws/connect/{userId}”)
    public class StandardWebSocketServer {

    // 在线用户会话池:线程安全
    private static final Map<Long, Session> ONLINE_SESSION_MAP = new ConcurrentHashMap<>();

    /**

    • 连接建立:鉴权、注册会话、上线补发离线消息
      */
      @OnOpen
      public 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. 上线触发离线消息补发(通过静态工具获取业务Service)
      try {
      MsgService msgService = SpringContextUtil.getBean(MsgService.class);
      msgService.pushOfflineMsg(userId);
      } catch (Exception e) {
      log.error(“用户{} 离线消息推送异常”, userId, e);
      }
      }

    /**

    • 接收客户端消息:区分心跳、业务消息
      */
      @OnMessage
      public 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);
      }
      }

    /**

    • 连接关闭回调,清理在线会话
      */
      @OnClose
      public void onClose(@PathParam(“userId”) Long userId, CloseReason reason) {
      ONLINE_SESSION_MAP.remove(userId);
      log.info(“用户{} 连接关闭,关闭码:{}”, userId, reason.getCloseCode());
      }

    /**

    • 连接异常回调,区分正常关闭与异常崩溃
      */
      @OnError
      public 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) 锁,串行发送;

避坑误区:加全局锁锁类,会导致所有用户消息排队,性能暴跌。

坑2:WebSocket无法注入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://(WebSocket+SSL),明文ws禁止外网访问;

Token短期有效期:JWT设置短过期时间,前端定时刷新Token,重连携带新凭证;

消息内容过滤:增加敏感词拦截、XSS过滤,防止前端注入恶意脚本;

关闭超时连接:限制单条连接空闲最大时长,自动回收资源。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/11 21:00:36

MAX77654与R7FA6M3AH3CFC的低功耗电源管理方案设计

1. 项目背景与核心需求 在嵌入式系统设计中&#xff0c;电源管理一直是决定产品可靠性和能效表现的关键环节。MAX77654是Maxim Integrated&#xff08;现已被ADI收购&#xff09;推出的一款高度集成的PMIC&#xff08;电源管理集成电路&#xff09;&#xff0c;而R7FA6M3AH3CFC…

作者头像 李华
网站建设 2026/7/11 21:00:31

我与 IT 这三十年:1996,屏幕还很小,世界刚变大

很多年以后&#xff0c;我再回想自己和 IT 的关系&#xff0c;总觉得它不是从互联网开始的。 它更像是从一台很重的电脑开始的。 大约 1994 年&#xff0c;我有了自己的第一台电脑。现在看配置&#xff0c;会觉得像博物馆标签&#xff1a;386DX40&#xff0c;4MB 内存&#x…

作者头像 李华
网站建设 2026/7/11 21:00:28

STM32F405RG与ISOM8710的高压隔离设计实战

1. 高压安全隔离的核心需求与选型逻辑 在工业自动化、电力电子和医疗设备领域&#xff0c;高压与低压电路间的安全隔离是生死攸关的设计要素。以电机控制系统为例&#xff0c;当STM32F405RG需要监测380V交流电机运行时&#xff0c;一次意外的电压浪涌若无隔离保护&#xff0c;可…

作者头像 李华
网站建设 2026/7/11 20:59:45

TB9051FTG与TM4C129XNCZAD实现静音电机控制方案

1. 项目背景与核心需求解析在医疗设备、智能家居和精密仪器等领域&#xff0c;直流电机的噪声问题一直是工程师们头疼的难题。传统PWM调速方案在低速运行时尤为明显&#xff0c;会产生令人不适的电磁噪声和机械振动。这正是TB9051FTG驱动芯片与TM4C129XNCZAD微控制器组合方案的…

作者头像 李华
网站建设 2026/7/11 20:59:21

OpenAI“二号人物”Fidji Simo离职,增长放缓下To-C与To-B业务统筹待解

Fidji Simo离职&#xff1a;从全职掌舵到兼职顾问7月10日&#xff0c;OpenAI的“二号人物”、AGI部署CEO Fidji Simo在内部信中宣布辞去全职职务&#xff0c;转为兼职顾问。外界原本预计她会在OpenAI上市后担任更重要领导角色。Simo在加入OpenAI前履历辉煌&#xff0c;曾担任Fa…

作者头像 李华