news 2026/10/26 4:03:07

SpringBoot WebSocket 消息推送 Demo:握手与会话管理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot WebSocket 消息推送 Demo:握手与会话管理

简介:这份 SpringBoot+WebSocket 示例工程面向需要为 Web 应用补充实时通信能力的后端开发者与入门学习者,围绕聊天室、实时通知一类场景,演示如何在 Spring Boot 中集成 WebSocket 并完成消息收发。工程包含 WebSocket 配置类与自定义处理器,完成连接建立、会话登记与消息广播,另外还附带可直接打开的前端页面,用于联调测试。整包共 106 个文件,以 82 个 xml(Maven 与工程配置)、5 个 java 源文件、5 个 class 编译产物、4 个 html 页面为主,另有 properties 配置、js 脚本以及 1 个 jar 依赖,压缩包约 169KB;体量轻便,导入 IDE 即可运行。已有 412 人学习,可见它作为起步模板有一定参考价值。读者可从中获得一套可运行的连接与广播骨架、前后端对接示例,以及在此基础上扩展身份校验、消息编解码与异常处理的着手点,适合作为实时功能开发的练手底稿。

1. 消息推送总在轮询上翻车,这个 Demo 把 WebSocket 讲透了

做过实时消息的开发者大概都有过这样的血泪经验:前端setInterval每隔三秒拉一次接口,数据延迟不说,服务端日志被轮询请求刷得密密麻麻,稍微上点量,数据库连接池就开始告急。这时候老手会告诉你,该上 WebSocket 了。但真去翻 SpringBoot 的 WebSocket 文档,WebSocketHandler、HandshakeInterceptor、WebSocketConfigurer一堆接口摆在面前,反而不知道从哪下手。

这份 SpringBoot+WebSocket Demo 就是来解决这个问题的。它把「浏览器与服务端建立长连接、双向收发消息」这条链路完整跑通,从前端页面到一个后端 Handler,从连接建立、会话管理到消息广播,一层层拆给你看。适合两类人:一是刚接触 WebSocket、想在本地先跑通一个能看见效果的最小项目的开发者;二是项目里正打算把轮询换成推送、需要一个可改造骨架的熟手。

2. 工程骨架与依赖:从 pom 到目录,看清这个 Demo 的骨架

拿到一个 SpringBoot Demo,我习惯先看两件事:pom.xml里引了什么,src下目录怎么摆。这两处基本决定了这个项目的技术边界和扩展上限,比读 README 靠谱得多。

2.1 依赖清单:spring-boot-starter-websocket 是唯一的关键项

这个 Demo 的核心依赖只有一个,就是 SpringBoot 官方提供的 WebSocket starter。它内部封装了 Spring 的 WebSocket 抽象层,并且把底层容器(内嵌 Tomcat)对 WebSocket 协议的支持一并带进来了,不需要额外引 Tomcat 的 websocket 包。

<dependencies> <!-- Web 基础:提供内嵌 Tomcat 与 SpringMVC --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 核心:Spring 的 WebSocket 抽象 + 容器对 WS 协议的支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <!-- 简化实体类,消息体 POJO 用 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

三个依赖各司其职。starter-web提供 HTTP 服务和静态资源映射,前端页面直接丢resources/static下就能访问,省掉单独起前端的麻烦。starter-websocket是主力,它把spring-websocket和spring-messaging拉进来,前者是原生 WebSocket 的接口层,后者是 STOMP 消息协议那一套。这个 Demo 走的是原生 Handler 路线,所以spring-messaging实际用不上,但它跟着传递依赖进来,不影响运行。

提示:如果你的项目只需要原生 WebSocket,不想被 STOMP 那套消息代理概念干扰,依赖引到starter-websocket就够了,不用再单独引spring-messaging,避免后面看源码时被两套模型绕晕。

Lombok 是可选项,只是为了让消息实体类少写 getter/setter。如果本地 IDE 没装 Lombok 插件,编译会报「找不到符号」,这时候要么装插件开注解处理,要么老老实实手写 getter,别在这上面卡半小时。

2.2 目录结构:五层划分,每层职责单一

标准 Maven 布局下,这个 Demo 的包结构大致是这样组织的:

src/main/java/com/example/websocketdemo/ ├── WebSocketDemoApplication.java # 启动类 ├── config/ │ └── WebSocketConfig.java # 注册 Handler 与拦截器 ├── handler/ │ └── ChatWebSocketHandler.java # 核心:连接、消息、关闭回调 ├── interceptor/ │ └── AuthHandshakeInterceptor.java # 握手阶段校验(可选扩展点) ├── session/ │ └── SessionManager.java # 会话池,管理在线连接 └── model/ └── Message.java # 消息体 POJO

config包负责把 Handler 挂到某个 URL 路径上,是「路由层」;handler包是业务核心,连接建立、收到消息、连接关闭三个回调都在这里;interceptor是握手阶段的钩子,用来做鉴权或参数提取;session单独抽出来管理会话集合,避免 Handler 里塞满静态 Map;model放消息结构。

resources下通常有个static/index.html,里面用原生WebSocketAPI 连上来,页面上几个输入框加一个消息列表,功能虽小但闭环完整。这种「一个 HTML 搞定前端」的做法在 Demo 里很常见,好处是不用装 Node 环境,双击就能跑。

2.3 启动类与配置:一个注解加一个 Bean

启动类没什么花活,标准写法:

@SpringBootApplication public class WebSocketDemoApplication { public static void main(String[] args) { SpringApplication.run(WebSocketDemoApplication.class, args); } }

真正要留意的是配置类。SpringBoot 的自动配置不会帮你注册 WebSocket 端点,必须自己实现WebSocketConfigurer,把 Handler 显式挂到路径上:

@Configuration @EnableWebSocket // 开启原生 WebSocket 支持 public class WebSocketConfig implements WebSocketConfigurer { @Autowired private ChatWebSocketHandler chatHandler; @Autowired private AuthHandshakeInterceptor authInterceptor; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(chatHandler, "/ws/chat") // 端点路径 .addInterceptors(authInterceptor) // 握手拦截器 .setAllowedOrigins("*"); // 跨域放行,生产环境要收紧 } }

@EnableWebSocket是关键注解,没有它,WebSocketConfigurer实现了也不会生效,端点根本不会注册,前端连上去直接 404,这是新手最容易踩的坑之一。addHandler的第二个参数是路径,前端连接时写ws://localhost:8080/ws/chat即可。

setAllowedOrigins("*")在开发阶段图省事,生产必须改成具体域名。这个参数控制的是 WebSocket 握手时的Origin头校验,写成*等于任何来源的页面都能连,配合 Cookie 场景会有安全风险,这一点在二次开发时要收掉。

3. 核心链路拆解:一个连接从握手到收发消息经历了什么

这一段是整篇最该细看的部分。WebSocket 看起来神秘,其实就三件事:握手把 HTTP 升级成 WS、连接建立后维护会话、消息来了分发给目标。把这个 Demo 的 Handler 读透,这三件事就都清楚了。

3.1 继承 TextWebSocketHandler:三个回调撑起全部业务

原生 WebSocket 的 Handler 有两种基类可选,TextWebSocketHandler处理文本帧,BinaryWebSocketHandler处理二进制帧。聊天、通知这类场景传 JSON 字符串,用前者就够。

@Component public class ChatWebSocketHandler extends TextWebSocketHandler { private static final Logger log = LoggerFactory.getLogger(ChatWebSocketHandler.class); @Autowired private SessionManager sessionManager; // 1. 连接建立成功 @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { // 从握手阶段塞进来的属性里取用户标识 String userId = (String) session.getAttributes().get("userId"); sessionManager.add(userId, session); log.info("连接建立, userId={}, sessionId={}, 当前在线={}", userId, session.getId(), sessionManager.onlineCount()); } // 2. 收到客户端消息 @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String payload = message.getPayload(); log.info("收到消息: {}", payload); // 解析目标用户与内容,这里用简单字符串协议演示,实际建议 JSON Message msg = JSON.parseObject(payload, Message.class); sessionManager.sendTo(msg.getTo(), msg.getContent()); } // 3. 连接关闭(正常关闭或异常断开都会进) @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { String userId = (String) session.getAttributes().get("userId"); sessionManager.remove(userId); log.info("连接关闭, userId={}, status={}, 当前在线={}", userId, status, sessionManager.onlineCount()); } }

三个回调分别对应连接的三个生命周期节点。afterConnectionEstablished在握手成功后立刻触发,此时session已可用,但注意还没经过任何业务处理,适合做「登记上线」。handleTextMessage是主要业务入口,所有客户端发来的文本都从这里进。afterConnectionClosed是清理点,不管是用户主动关闭、网络断开还是服务端踢人,都会走到这里,是释放资源的唯一可靠时机。

session.getAttributes()是贯穿三处的那根线。它不是 WebSocket 协议自带的东西,而是本地构建的Map<String, Object>,在握手拦截器里塞值,在后续回调里取值。这个机制很重要——WebSocket 连接本身不带身份信息,身份只能靠握手阶段注入,比每次消息都带 token 靠谱。

还有一个容易忽略的点:handleTextMessage里没有throws之外的异常处理。如果JSON.parseObject抛异常,消息会被吞掉,客户端看不到任何反馈。实际项目里这里要包 try-catch,失败时回一条错误消息给发送方,否则排查问题时就是黑匣子。

3.2 会话管理:ConcurrentHashMap 才是正解

多人在线就得存会话。这个 Demo 把会话池单独拆成SessionManager,是很清醒的做法,避免 Handler 里出现一个private static Map又当爹又当妈。

@Component public class SessionManager { // key 用业务用户 ID,value 是这条连接的会话 // 必须并发容器:WebSocket 的回调线程来自容器线程池,天然多线程 private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>(); public void add(String userId, WebSocketSession session) { sessions.put(userId, session); } public void remove(String userId) { sessions.remove(userId); } public int onlineCount() { return sessions.size(); } // 发给指定用户 public void sendTo(String userId, String content) throws IOException { WebSocketSession target = sessions.get(userId); if (target != null && target.isOpen()) { // WebSocketSession 不是线程安全的,发送必须加锁 synchronized (target) { target.sendMessage(new TextMessage(content)); } } } // 广播给所有人 public void broadcast(String content) throws IOException { for (WebSocketSession s : sessions.values()) { if (s.isOpen()) { synchronized (s) { s.sendMessage(new TextMessage(content)); } } } } }

容器选型上,ConcurrentHashMap不是可选项而是必须项。WebSocket 的每个回调都由容器线程池里的线程执行,两个用户同时上线,add就是并发的,用HashMap轻则数据丢失重则死循环,这种玄学问题一旦出现,日志里根本看不出来。

sendTo里那层synchronized (target)更是血泪经验。WebSocketSession的 javadoc 明确写了自己不是线程安全的,而 Spring 官方的ConcurrentWebSocketSessionDecorator会强制串行化写入。如果两个线程同时往同一个 session 发消息,底层 TCP 写出会交错,客户端收到的可能是半截 JSON 拼着另一条消息的头,典型的「消息错乱」。加锁是最小的修复成本,代价是高并发下单个 session 的吞吐受限,量大再换成消息队列异步派发。

isOpen()的判断也不能省。连接刚断但afterConnectionClosed还没执行的那一瞬间,session 还在 Map 里但已经不可写,直接 send 会抛IOException,把整个循环带崩。广播里尤其明显——一个人掉线,其他人全收不到,这类事故排查起来极其费时。

3.3 握手拦截器:把身份注入 attributes

WebSocket 不像 HTTP 那样每个请求都带 Cookie/Header,连接建立一次就长期复用。所以鉴权只能在握手那一下完成,完成后把身份存起来,后续消息靠 session 关联。

@Component public class AuthHandshakeInterceptor implements HandshakeInterceptor { // 握手之前:此时还是 HTTP 请求,能拿到 header 和参数 @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { // 演示:从 URL 参数取 userId,实际项目应从 token 解析 String query = request.getURI().getQuery(); String userId = extractParam(query, "userId"); if (userId == null || userId.isEmpty()) { // 返回 false 会直接拒绝握手,前端 onerror 触发 return false; } attributes.put("userId", userId); // 塞进去,后面回调里能取 return true; } // 握手之后:一般用来记日志或做统计,很少改状态 @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { // no-op } private String extractParam(String query, String key) { if (query == null) return null; for (String pair : query.split("&")) { String[] kv = pair.split("="); if (kv.length == 2 && key.equals(kv[0])) return kv[1]; } return null; } }

beforeHandshake是唯一能在连接建立前做拦截的地方。返回true放行,返回false拒绝。注意拒绝后前端收到的是握手失败,不是正常的关闭帧,onerror会触发,但onclose也会跟着触发,前端状态机要按这个顺序处理,否则会出现「连接失败但 UI 显示已断开」的错位。

attributes这个 Map 是拦截器和 Handler 之间的唯一通道。它的生命周期和 session 一致,session 关闭后自动失效,不需要手动清理。这也意味着不能在 attributes 里塞大对象,会话多了内存会持续占用,塞个 userId 这种短字符串最合适。

示例里从 URL 参数取 userId 只是为了 Demo 好演示。实际项目里 token 通常放在 header 或者Sec-WebSocket-Protocol子协议里,浏览器原生 WebSocket API 不支持自定义 header,所以常见做法是把 token 拼在 URL query 上(注意日志脱敏)或者用子协议字段承载,这两种方案各有取舍,前者简单但 token 容易进访问日志。

3.4 前端这一侧:原生 API 二十行搞定

前端页面不用框架也能跑,原生WebSocket对象够用了:

// 连接时把身份带上,服务端拦截器从 query 里取 const userId = 'user_' + Math.floor(Math.random() * 1000); const ws = new WebSocket(`ws://localhost:8080/ws/chat?userId=${userId}`); // 连接打开,可以发消息了 ws.onopen = () => { console.log('已连接'); appendMsg('系统', '连接成功'); }; // 收到服务端推送 ws.onmessage = (event) => { const data = JSON.parse(event.data); appendMsg(data.from || '对方', data.content); }; // 出错和关闭要分开处理 ws.onerror = () => appendMsg('系统', '连接出错'); ws.onclose = (e) => appendMsg('系统', `连接关闭 code=${e.code}`); // 发送按钮 document.getElementById('sendBtn').onclick = () => { const input = document.getElementById('msgInput'); const payload = JSON.stringify({ to: 'user_1', content: input.value }); // 发送前必须检查状态,CONNECTING 阶段直接 send 会抛异常 if (ws.readyState === WebSocket.OPEN) { ws.send(payload); input.value = ''; } };

四个事件回调对应连接全生命周期,onopen之后才能send,这是铁律。readyState有四个值:CONNECTING(0)、OPEN(1)、CLOSING(2)、CLOSED(3),只有等于 1 时发送才合法。新手最常见的翻车就是页面一加载就send,那时还在握手,直接抛InvalidStateError。

断线重连这里没实现,Demo 层面可以理解,但真实项目必须补。标准做法是在onclose里起一个带退避的setTimeout重连,间隔按 1s、2s、4s、8s 递增并设上限,同时用一个标志位防止重连风暴。

4. 本地跑通:三步启动与四个必验点

代码看懂了,接着要把它在本地跑起来。这个 Demo 的好处是没有外部依赖,不需要数据库、不需要 MQ,一个 JDK 加 Maven 就能完整验证。

4.1 环境与启动命令

环境要求不高:JDK 8 及以上(SpringBoot 2.x 系列),Maven 3.6+。如果你用的是 SpringBoot 3.x 分支,JDK 必须是 17 起,这一点先确认清楚,否则启动直接报UnsupportedClassVersionError,报错信息还不在第一屏。

# 进入项目根目录,先编译 mvn clean package -DskipTests # 方式一:直接跑 jar java -jar target/websocket-demo-0.0.1-SNAPSHOT.jar # 方式二:开发期用插件跑,改代码不用重新打包 mvn spring-boot:run

启动成功后控制台会打出Tomcat started on port(s): 8080和Started WebSocketDemoApplication。端口被占用是最常见的启动失败原因,改application.yml里的server.port即可,别去翻代码。

server: port: 8080 logging: level: com.example.websocketdemo: debug # 打开自己包的日志,方便看连接回调

日志级别调到 debug 后,握手、消息、关闭的每个回调都会打出来,排查连接问题时这比断点还快。

4.2 四个必验点:从单连接走到双人互推

光看到启动日志不算跑通,要按顺序验证四件事:

第一,页面能打开。浏览器访问http://localhost:8080/index.html,能看到输入框和消息区。看不到就是静态资源路径不对,检查文件是否在src/main/resources/static/下,注意不是templates,后者要走模板引擎渲染,直接访问会 404。

第二,单连接能建立。打开浏览器开发者工具的 Network 面板,筛选 WS,刷新页面应该能看到一条状态 101 的请求。101 是Switching Protocols,看到它说明 HTTP 成功升级为 WebSocket。如果一直是 pending 然后失败,八成是拦截器返回了 false 或者路径写错。

第三,消息能双向走。在输入框发一条,看控制台是否打出「收到消息」,同时页面上是否出现自己发的这条。服务端收到但页面不回显,那是前端onmessage里的解析出了问题;页面有回显但服务端无日志,那是连接根本没通。

第四,双人互推能成。开两个浏览器窗口(用无痕模式避免 session 冲突),各自连上,A 发给 B,B 的页面要能收到。这一步验证的是SessionManager的路由逻辑,也是这个 Demo 最核心的价值点。如果 B 收不到,先看两个窗口的 userId 是不是撞了——随机数方案在小样本下撞车概率不低。

注意:用一个浏览器开两个标签页测试时,某些浏览器会对同域名连接做复用,表现为第二个标签的连接状态异常。测试多连接一律用无痕窗口或不同浏览器,能省掉大量误判。

5. 避坑与排查:五个真实踩过的坑

WebSocket 的问题有个共同特点:现象和原因隔得远,日志还经常不给线索。下面五条是我在类似 Demo 上真实翻过的车,每条按现象、原因、解决三段写,遇到时可以直接对号。

坑一:前端一直报 404,Network 里连 WS 请求都看不到。现象是new WebSocket()后立刻onerror,控制台提示握手失败。原因通常是配置类没生效——要么漏了@EnableWebSocket,要么WebSocketConfig没被扫描到(包路径不在启动类的同级或子包下)。还有一种可能是版本问题,SpringBoot 3.x 里WebSocketConfigurer的包路径变过,从旧教程抄的 import 会编译不过或运行时不识别。解决:先确认启动日志里有没有端点注册的痕迹,再检查注解和包路径,最后核对 SpringBoot 大版本与教程是否一致。

坑二:连接建立成功,但服务端一收消息就断。现象是afterConnectionEstablished打了日志,客户端发第一条消息后连接立刻关闭,服务端没有任何异常堆栈。原因是handleTextMessage里抛了未捕获异常,Spring 的默认行为是记录日志后关闭连接,而日志级别不够时你看不到。解决:把logging.level.org.springframework.web.socket=debug打开,或者在 handler 里整体包 try-catch 把异常打出来。这类问题九成是 JSON 解析失败——客户端发的不是合法 JSON,或者字段名和 POJO 对不上。

坑三:双人测试时 B 收不到消息,但日志显示消息已发出。现象是sendTo执行了没报错,B 的页面就是没反应。原因有两个方向:一是SessionManager的 key 撞了,两个连接用了同一个 userId,后连的把先连的覆盖掉,消息发给了自己;二是WebSocketSession并发写导致帧损坏,客户端解析失败但没报错。解决:在add方法里加个判断,key 已存在时先关旧连接再放新连接,并把覆盖事件打日志;并发写的问题按前面说的加synchronized或换ConcurrentWebSocketSessionDecorator。

坑四:本地好好的,部署到服务器后连接频繁断。现象是连接能建立,但空闲一段时间后必断,重连又正常。原因基本是中间层(反向代理、负载均衡)的空闲超时,默认 60 秒没数据就掐连接,而 WebSocket 空闲时确实没数据。解决:服务端加心跳,定时给客户端发 ping 帧或者业务层的心跳包,间隔设在超时时间的一半以内;同时在代理层把proxy_read_timeout调大。心跳这事在 Demo 里可以不做,上生产必须做,否则用户看到的就是「页面放着不动就掉线」。

坑五:Chrome 能连,某些浏览器或客户端连不上。现象是换浏览器或者用 Postman、移动端 SDK 连,握手直接被拒。原因通常是setAllowedOrigins配得太死,或者某些客户端不发Origin头,Spring 的默认校验会拒绝无 Origin 的请求。解决:生产环境按域名白名单配置,不要图省事写*;对接非浏览器客户端时,确认对方是否带 Origin,必要时对特定路径放开校验。这条坑的隐蔽性在于「同一份代码在不同客户端表现不同」,很容易被误判成前端 bug。

6. 二次开发改造:从 Demo 到能上生产的三个方向

Demo 跑通只是起点,真拿去用,至少要补三块:消息协议、连接健壮性、水平扩展。这三块决定了它是「能演示」还是「能用」。

6.1 消息协议标准化:从裸字符串到带类型的信封

原始 Demo 直接传 content,消息没有类型,扩展时会很痛苦。我一般会先定义一层信封结构:

@Data public class WsMessage { private String type; // CHAT / PING / SYSTEM / ACK private String from; // 发送方 private String to; // 接收方,广播时为 null private String content; // 业务载荷 private long timestamp; // 客户端可用于排序和去重 }

对应 Handler 里按type分发,CHAT走业务、PING直接回PONG不落库、ACK用于确认送达。加这一层的好处是后续加新消息类型不用改分发骨架,只加分支。心跳也顺带解决了——前端每 30 秒发一次PING,服务端回PONG,既保活又能探测半开连接。

6.2 连接健壮性:心跳、重连、会话清理三件套

心跳前面提过,服务端侧可以用 Spring 的TaskScheduler定时遍历会话发 ping,客户端侧在onmessage里刷新一个本地计时器,超过阈值没收到任何消息就主动重连。

重连要带退避,别用固定间隔:

let retry = 0; const MAX_RETRY = 5; function connect() { const ws = new WebSocket(url); ws.onopen = () => { retry = 0; }; // 连上就重置计数 ws.onclose = () => { if (retry >= MAX_RETRY) return; // 超过上限停止,提示用户 const delay = Math.min(1000 * 2 ** retry, 30000); // 指数退避,封顶 30s retry++; setTimeout(connect, delay); }; }

指数退避的意义在于避免服务端重启时所有客户端同时重连,形成惊群。封顶 30 秒是经验值,再长用户会觉得卡死。

会话清理这块,afterConnectionClosed是主路径,但不能只依赖它。进程被 kill、容器 OOM 时这个回调不会执行,Map 里的脏会话会一直留着。稳妥做法是给每个 session 记录最后活跃时间,起一个定时任务扫掉超时的,双保险。

6.3 水平扩展:单机 Map 撑不住时怎么办

SessionManager用本地 Map 存会话,单机没问题,一旦部署两个实例,A 实例上的用户给 B 实例上的用户发消息就找不到了。这是从 Demo 走向生产的最大一道坎。

常见做法是引入 Redis 做会话路由:每个实例启动时订阅一个频道,用户上线时把userId -> 实例标识写进 Redis,发消息时先查目标用户在哪个实例,不在本机就投递到对应频道由目标实例转发。另一种更彻底的做法是直接上 STOMP + 外部消息代理,用 broker 做广播,Spring 的spring-messaging就是为这个准备的。

改造成本上,前一种方案改动小,只动SessionManager和加一个 Redis 订阅;后一种要重写 Handler 层,把原生帧换成 STOMP 帧。选哪种看规模——几千连接以内,Redis 路由足够;再往上、还要做集群广播,STOMP 那套更省心。

提示:改造成 Redis 路由时,用户下线要记得删路由键,并且给键设 TTL 兜底。否则实例崩溃后残留的路由会让消息发到不存在的实例,表现为「消息发出去没人收」,比直接报错更难查。

最后说个验证方法:改造完别只在本地测,起两个实例跑在不同端口,用两个浏览器分别连不同端口,互发消息验证跨实例路由。这一步能提前暴露八成以上的集群问题,比上线后再查划算得多。从那以后我每次做 WebSocket 相关的改造,都会强制走一遍「双实例互推」这个验证,因为单机测试永远测不出分布式下的会话丢失。希望帮到你。

本文还有配套的精品资源,点击获取

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

Linux服务器RAID配置与硬件选型实战指南

开机上架一台新服务器&#xff0c;很多人第一件事就是插盘、装系统&#xff0c;等到业务跑起来才发现 IO 抖得厉害、重建一次要十几个小时&#xff0c;甚至阵列崩了数据拿不出来。问题往往不在系统层面&#xff0c;而在最底下那层——Linux服务器硬件选型与RAID配置这件事上。我…

作者头像 李华
网站建设 2026/10/26 4:03:00

Stable Diffusion原理详解:CLIP文本编码、扩散模型与UNet文生图实战

Stable Diffusion 原理详解&#xff1a;从 CLIP、扩散模型、UNet 到文生图代码实战&#xff08;四集精讲笔记&#xff09;1. 基础概念&#xff1a;这套模型到底在做什么在我最开始接触 Stable Diffusion 的时候&#xff0c;有个误区非常普遍&#xff1a;以为它是一个类似 GAN 的…

作者头像 李华
网站建设 2026/10/26 4:02:13

企业级数据可视化平台构建与Superset实战指南

1. 为什么企业需要自建数据可视化平台&#xff1f;在数字化转型浪潮中&#xff0c;数据可视化已成为企业决策的刚需。传统报表工具如Excel在面对TB级数据时明显力不从心&#xff0c;而商业BI工具又存在授权费用高、扩展性差的问题。Apache Superset作为开源解决方案&#xff0c…

作者头像 李华
网站建设 2026/10/26 4:02:04

AI时代前端开发新范式:TRAE SOLO模式实践

1. 项目概述&#xff1a;AI时代的前端开发新范式最近在重构一个老项目的前端界面时&#xff0c;我尝试了全新的开发模式——将传统前端设计与AI编程工具深度结合。这种被称为"TRAE SOLO"的工作方式&#xff0c;彻底改变了我对前端开发的认知。简单来说&#xff0c;它…

作者头像 李华
网站建设 2026/10/26 4:01:58

SAP PP模块中BOM主数据的核心应用与优化实践

1. BOM主数据在SAP PP模块中的核心定位BOM&#xff08;Bill of Materials&#xff09;作为SAP PP模块的核心主数据&#xff0c;直接决定了企业生产计划的准确性和物料需求计算的可靠性。在15年的SAP实施经验中&#xff0c;我见过太多企业因为BOM数据管理不当导致MRP跑出荒谬结果…

作者头像 李华
网站建设 2026/10/26 4:01:52

深度剖析C++对象池自动回收技术实现

前言 对象池&#xff08;object pool&#xff09;要解决的问题是&#xff1a;某类对象会被频繁创建和销毁&#xff0c;而每次 new / delete 都要进入分配器、可能触发系统调用、还可能造成堆碎片。对象池预先申请一批槽位&#xff0c;创建时复用空闲槽&#xff0c;销毁时不真正…

作者头像 李华