尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
SpringBoot WebSocket 实战:长连接、心跳重连与避坑
简介面向 SpringBoot 初学者以及需要实现实时通信功能的开发者这份 SpringBootWebSocket Demo 以可运行的示例工程演示如何在 Spring Boot 项目中接入 WebSocket完成连接建立、会话维护与消息广播可作为聊天室、在线通知等场景的入门骨架技能层级偏基础到中级。压缩包共 106 个文件约 169KB以 82 个 xml 配置文件、5 个 java 源码及对应 class、4 个 html 测试页面为主另含 properties 配置、js 脚本与 jar、mvnw 等构建文件工程结构符合 Maven 项目规范便于直接导入 IDE 调试。已有 412 人学习这份资源。示例覆盖 WebSocket 配置类、自定义消息处理器与原生 HTML 客户端能帮助读者理解连接注册、消息转发和服务端广播的完整链路并在此基础上扩展鉴权、编解码与异常处理等功能。1. 从轮询到长连接SpringBootWebSocket Demo 解决的是什么问题后台系统里最常见的伪实时方案是定时轮询前端每三秒请求一次未读数接口量小的时候看着挺正常一旦在线人数上去数据库连接池先扛不住接口响应从 80ms 抖到 800ms用户看到的还是「消息延迟」。SpringBoot WebSocket 就是把这层轮询掐掉的标准解法——服务端主动推、连接只建一次、消息到达即渲染。这份 Demo 把两条主流路线都放进去了原生 WebSocketHandler 和 STOMP 消息代理配套会话注册表、心跳空闲检测、断线重连和反向代理配置。适合正在给管理后台加消息中心、给大屏加实时刷新、给工单系统加状态推送的开发者也适合那些 Demo 跑通了但一上生产就掉线、一多实例就哑火的人。2. 骨架拆解一份 pom 与三个配置类怎么把端点跑起来拿到一个 WebSocket Demo最忌讳上来就mvn spring-boot:run然后盯着控制台看有没有报错。这个包的结构很清晰核心就四个文件pom.xml、一个WebSocketConfig、一个握手拦截器、一个消息处理器。先把这四个位置认全后面所有排错都能定位到具体层。2.1 依赖对齐spring-boot-starter-websocket 里到底装了什么很多人以为要单独引spring-websocket和spring-messaging其实一个 starter 就够了它已经把这两个传递依赖带进来。Demo 的 pom 只有一段关键配置版本统一交给 parent 管不要自己写version这是版本冲突的头号来源。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency !-- 只有走 STOMP SockJS 路线时才需要原生 WebSocket 不用引 -- dependency groupIdorg.webjars/groupId artifactIdsockjs-client/artifactId version1.5.1/version /dependency逻辑说明spring-boot-starter-websocket内部依赖spring-websocket提供WebSocketHandler、HandshakeInterceptor和spring-messaging提供 STOMP 的MessageMapping、SimpMessagingTemplate。也就是说只要你引了这一个两条路线都能写不需要额外操作。参数说明sockjs-client的 webjars 版本要和你前端实际使用的 SockJS 版本一致不一致会出现握手成功但帧解析失败。如果前端用 npm 装sockjs-client这个依赖直接删掉别留在 pom 里当摆设。这里有一处版本分水岭必须提前说清楚Spring Boot 2.x 用的是javax.websocket包3.x 换成了jakarta.websocket。如果你从网上抄了一段ServerEndpoint的 JSR-356 写法贴进 3.x 工程编译期就会报找不到javax.websocket.server.ServerEndpoint。这个 Demo 走的是 Spring 自己的WebSocketHandler体系不碰 JSR-356 注解所以跨 2.x/3.x 迁移时只要改包名和少量 API比ServerEndpoint那套省心得多。我一般的做法是新项目一律用WebSocketHandler或 STOMPServerEndpoint只在维护老系统时才碰。2.2 WebSocketConfig 注册端点三个参数决定握手能不能成端点是客户端连接的入口地址注册错了后面全白搭。Demo 里的配置类长这样Configuration EnableWebSocket public class RawWsConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(orderHandler(), /ws/order) // 端点路径 .addInterceptors(new AuthHandshakeInterceptor()) // 握手拦截器 .setAllowedOriginPatterns(*); // 跨域白名单 } Bean public WebSocketHandler orderHandler() { return new OrderWebSocketHandler(); } }逻辑说明EnableWebSocket是把 Spring 的WebSocketHandlerMapping装配进来的开关漏了它就只是一个普通配置类端点根本不会注册客户端连上去直接 404。addHandler的第一个参数是处理器实例第二个是路径这个路径是相对于server.servlet.context-path的。参数说明setAllowedOriginPatterns(*)和setAllowedOrigins(*)不是同一回事。Spring 5.3 之后setAllowedOrigins(*)在带 cookie 或凭证的场景下会被拒绝必须换成setAllowedOriginPatterns。Demo 里为了开箱即用写了*上生产前一定要收敛成具体域名比如https://admin.example.internal否则等于把连接入口对整个互联网敞开。还有一个容易忽略的点如果你的工程配了context-path: /demo那客户端连的完整地址是ws://host:8080/demo/ws/order漏掉/demo这一段就是 404。血泪经验是——把端点路径、context-path、网关前缀三段写在同一张纸上贴屏幕边上比在浏览器控制台猜半天强。2.3 握手拦截器浏览器不让带 header身份怎么传进去这是新手最容易卡住的地方。浏览器的new WebSocket(url)构造函数只接受两个参数第二个是子协议没有 headers 选项。你想学 HTTP 那样塞一个Authorization: Bearer xxx做不到。所以身份信息只能走两条路URL query 参数或者Sec-WebSocket-Protocol子协议。public class AuthHandshakeInterceptor implements HandshakeInterceptor { Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, MapString, Object attributes) { // 从 query 参数取 tokenws://host/ws/order?tokenxxx String token UriComponentsBuilder.fromUri(request.getURI()) .build().getQueryParams().getFirst(token); if (token null || token.isBlank()) { response.setStatusCode(HttpStatus.UNAUTHORIZED); return false; // 返回 false 直接拒绝握手客户端收到 401 } Long uid tokenResolver.resolve(token); // 换成你自己的校验逻辑 if (uid null) { response.setStatusCode(HttpStatus.UNAUTHORIZED); return false; } attributes.put(uid, uid); // 关键塞进会话属性 return true; } Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { // 握手成功后的埋点位置不要在这里做重活 } }逻辑说明attributes这个 Map 是握手层和业务层的唯一通道。你在beforeHandshake里放进去的uid会在afterConnectionEstablished里通过session.getAttributes().get(uid)取出来。这条链路打通了后面所有「按用户推消息」的逻辑才有根。参数说明token 放在 query string 里会被 Nginx 的 access log 完整记录下来属于凭据泄露风险点。常见的缓解做法是用短时效 token比如 5 分钟有效、用完即废的一次性票据或者改走Sec-WebSocket-Protocol子协议传参。后者需要服务端在响应头里回显同一个子协议名否则浏览器会主动断开连接——这个细节不写对现象就是「握手 101 成功紧接着连接被关闭」很玄学。3. 两条路线选型原生 WebSocketHandler 与 STOMP 分别适合谁Demo 里两套代码并存不是炫技是因为它们的适用面完全不同。原生 Handler 贴近协议层你自己定义消息格式、自己管订阅关系灵活但活多STOMP 是建立在 WebSocket 之上的文本协议自带订阅/目的地语义Spring 还送了SimpMessagingTemplate写起来像发 MQ。选错的代价不小改造成本比一开始选对高得多。3.1 原生 Handler 的四个回调消息收发与会话状态继承TextWebSocketHandler之后你真正要关心的是四个方法每个对应一种生命周期状态Component public class OrderWebSocketHandler extends TextWebSocketHandler { private final SessionRegistry registry; public OrderWebSocketHandler(SessionRegistry registry) { this.registry registry; } Override public void afterConnectionEstablished(WebSocketSession session) { Long uid (Long) session.getAttributes().get(uid); registry.add(uid, session); // 会话入册 } Override protected void handleTextMessage(WebSocketSession session, TextMessage message) { String payload message.getPayload(); // 收到的原文 // 应用层心跳约定客户端每 25s 发一次 ping if (ping.equals(payload)) { sendSafely(session, pong); return; } // 业务消息这里做命令分发 } Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { Long uid (Long) session.getAttributes().get(uid); registry.remove(uid, session); // 一定记得摘除 } Override public void handleTransportError(WebSocketSession session, Throwable exception) { registry.remove((Long) session.getAttributes().get(uid), session); // 传输层异常基本等同于连接不可用直接清账 } private void sendSafely(WebSocketSession session, String text) { try { if (session.isOpen()) { session.sendMessage(new TextMessage(text)); } } catch (IOException e) { // 发送失败不要抛出去交给上层重连逻辑 } } }逻辑说明afterConnectionEstablished是会话注册的时机afterConnectionClosed是摘除的时机这两个必须成对出现。handleTransportError很多人不重写结果就是网络闪断的会话永远留在 Map 里内存缓慢上涨一周后 OOM排查时完全没有线索——这就是典型的黑匣子问题。参数说明sendMessage是阻塞调用而且WebSocketSession不是线程安全的。同一秒内两个线程同时往一个 session 发消息会抛IllegalStateException: The remote endpoint was in state [TEXT_PARTIAL_WRITING]。Demo 里的做法是用ConcurrentWebSocketSessionDecorator包一层// 包装成线程安全的会话三个参数分别是原会话、发送超时、缓冲区上限 WebSocketSession safe new ConcurrentWebSocketSessionDecorator(session, 5000, 64 * 1024);第一个参数是原始会话第二个是单次发送超时毫秒第三个是发送缓冲区字节上限。超过缓冲区上限时装饰器会直接关闭连接而不是无限堆积——这是保护服务端内存的正确姿势比你自己写队列加锁稳。3.2 STOMP 的 MessageMapping订阅模型与用户级推送如果你要的是「订阅某个主题的人都能收到」或者「只推给某个用户」STOMP 能省掉大量自己造轮子的时间。Configuration EnableWebSocketMessageBroker public class StompConfig implements WebSocketMessageBrokerConfigurer { Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint(/ws/stomp) .setAllowedOriginPatterns(*) .withSockJS(); // 不支持原生 WS 的客户端走 SockJS 兜底 } Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker(/topic, /queue) .setHeartbeatValue(new long[]{10000, 10000}) // 服务端→客户端 10s客户端→服务端 10s .setTaskScheduler(heartbeatScheduler()); registry.setApplicationDestinationPrefixes(/app); // 客户端发消息的前缀 registry.setUserDestinationPrefix(/user); // 点对点推送前缀 } Bean public TaskScheduler heartbeatScheduler() { ThreadPoolTaskScheduler scheduler new ThreadPoolTaskScheduler(); scheduler.setPoolSize(1); scheduler.setThreadNamePrefix(ws-heartbeat-); scheduler.initialize(); return scheduler; } }逻辑说明enableSimpleBroker启用的是 Spring 内置的内存消息代理/topic一般用于广播/queue用于点对点。setApplicationDestinationPrefixes(/app)决定了客户端发消息时服务端MessageMapping的映射前缀。比如客户端发往/app/order/status就会命中MessageMapping(/order/status)的方法。参数说明setHeartbeatValue那个数组是{服务端发送间隔, 服务端期望接收间隔}单位毫秒。这个配置不配TaskScheduler是不生效的很多人只写了setHeartbeatValue结果心跳根本没跑连接被中间设备 60 秒掐断然后回来问「为什么 STOMP 一分钟后自动断」。这是踩坑率极高的一个组合配置。用户级推送靠SendToUserMessageMapping(/order/subscribe) SendToUser(/queue/order) // 只有发起订阅的用户能收到 public String subscribe(Payload String body, Principal principal) { return subscribed: principal.getName(); }Principal从哪来从握手阶段的DefaultHandshakeHandler里注入。如果你用 query token 的方式做鉴权需要自定义HandshakeHandler覆写determineUser把attributes里的 uid 转成Principal返回否则SendToUser拿到的principal是 null消息发出去没人收。3.3 前端连接代码URL、子协议与心跳字段后端配好了前端一行写错照样连不上。原生路线的连接代码// 注意ws 协议不是 httpcontext-path 要带上 const token encodeURIComponent(localStorage.getItem(access_token)); const ws new WebSocket(ws://127.0.0.1:8080/demo/ws/order?token${token}); ws.onopen () { console.log(connected); // 应用层心跳25s 一次必须小于网关的 idle 超时 timer setInterval(() ws.readyState 1 ws.send(ping), 25000); }; ws.onmessage (evt) { if (evt.data pong) return; // 心跳回包不渲染 renderMessage(JSON.parse(evt.data)); }; ws.onclose (evt) { clearInterval(timer); console.warn(closed, evt.code, evt.reason); };逻辑说明readyState 1表示OPEN发送前必须判断否则向一个CLOSING状态的连接写数据会抛异常而这个异常在部分浏览器里不会冒泡到控制台看起来就是「代码没执行」。参数说明协议头是ws://如果页面本身是 HTTPS浏览器会强制要求wss://混用会被直接拦截并报 mixed content这个错误在控制台里往往只显示一行很容易被忽略。生产环境记得把地址改成相对协议const proto location.protocol https: ? wss : ws; const ws new WebSocket(${proto}://${location.host}/demo/ws/order?token${token});STOMP 路线的前端用stomp/stompjsv7 起已移除内置 SockJS需要单独装sockjs-client并注入webSocketFactoryimport { Client } from stomp/stompjs; import SockJS from sockjs-client; const client new Client({ webSocketFactory: () new SockJS(/demo/ws/stomp), reconnectDelay: 5000, // 框架自带重连别自己再写一套 heartbeatIncoming: 10000, // 要和后端 setHeartbeatValue 的第二个值对齐 heartbeatOutgoing: 10000, // 要和后端第一个值对齐 connectHeaders: { Authorization: Bearer ${token} } // STOMP 帧头可以带不受浏览器限制 }); client.onConnect () { client.subscribe(/user/queue/order, (msg) renderMessage(JSON.parse(msg.body))); }; client.activate();这里有个反直觉的点STOMP 的帧头是可以带自定义字段的因为它在 WebSocket 建立之后走应用层协议浏览器那层限制管不到它。所以 STOMP 路线反而更适合放 token。心跳的两个值必须和后端交叉对齐写错了现象是「连接建立但每隔一段时间自动断开重连」日志里只有一行Whoops! Lost connection to ...。4. 会话、心跳与重连让消息推得出去也推得准连接建立只是起点。真正决定这个 Demo 能不能上生产的是三个东西会话怎么存、超时怎么设、断了怎么接回来。这三块没做对功能演示时一切正常一压测就原形毕露。4.1 会话注册表key 用 uid 还是 sessionId单机场景下用ConcurrentHashMapLong, WebSocketSession就够了但要注意同一用户可能在多个标签页、多台设备上登录所以 value 应该是 Set。Component public class SessionRegistry { // uid - 该用户的所有活跃会话 private final MapLong, SetWebSocketSession userSessions new ConcurrentHashMap(); public void add(Long uid, WebSocketSession session) { userSessions.computeIfAbsent(uid, k - ConcurrentHashMap.newKeySet()) .add(session); } public void remove(Long uid, WebSocketSession session) { SetWebSocketSession set userSessions.get(uid); if (set null) return; set.remove(session); if (set.isEmpty()) { userSessions.remove(uid); // 防止空 Set 长期占用内存 } } public SetWebSocketSession get(Long uid) { return userSessions.getOrDefault(uid, Collections.emptySet()); } // 广播给全部在线用户 public void broadcast(String text) { userSessions.values().forEach(set - set.forEach(s - sendQuietly(s, text))); } private void sendQuietly(WebSocketSession session, String text) { try { if (session.isOpen()) session.sendMessage(new TextMessage(text)); } catch (IOException ignored) { // 单条失败不影响其他会话 } } }逻辑说明computeIfAbsent配合ConcurrentHashMap.newKeySet()是并发安全的写法避免两个线程同时首次注册同一 uid 时创建两个 Set 导致会话丢失。remove里判断空集后删除 key是防止长期运行下 Map 里堆积大量空壳条目。参数说明key 用 uid 还是 sessionId取决于你的推送模式。按人推点对点用 uid按连接推比如运维需要精准踢掉某一个连接用 sessionId。Demo 里两者都存了一份主键用 uid另有一个MapString, Long做 sessionId 到 uid 的反查用于「强制下线」这类运维操作。发送失败的处理原则单条失败不要抛也不要重试。WebSocket 的失败通常意味着连接已死重试只会拖慢其他会话的推送。正确的做法是记录一条日志把会话从注册表摘掉等客户端的重连机制自己接回来。4.2 三个超时参数容器空闲、STOMP 心跳、网关读超时掉线问题九成出在超时参数没对齐。一份完整链路上有至少三个超时点任何一个比另一个短都会造成「莫名断开」。层级参数Demo 取值作用设短的后果WebSocket 容器maxSessionIdleTimeout120000 ms会话空闲多久被容器回收无消息时被强制关闭STOMP 心跳setHeartbeatValue10000 / 10000 ms双向心跳间隔中间设备判定连接僵死反向代理proxy_read_timeout300 s代理等待后端数据的时长60s默认后静默断连容器层的配置写法Bean public ServletServerContainerFactoryBean createWebSocketContainer() { ServletServerContainerFactoryBean container new ServletServerContainerFactoryBean(); container.setMaxTextMessageBufferSize(8192); // 单条文本消息上限 8KB container.setMaxBinaryMessageBufferSize(8192); // 单条二进制消息上限 8KB container.setMaxSessionIdleTimeout(120000L); // 空闲 2 分钟回收 container.setAsyncSendTimeout(5000L); // 异步发送超时 5s return container; }逻辑说明这四个参数控制的是容器Tomcat / Jetty / Undertow对 WebSocket 的底层约束。maxTextMessageBufferSize不设或设太小客户端发一条稍大的 JSON 就会收到CloseStatus 1009消息过大。setMaxSessionIdleTimeout在不同容器上的默认值不一样显式写死比依赖默认值靠谱得多。参数说明空闲超时和心跳机制是互相配合的。如果 STOMP 心跳 10 秒一次那maxSessionIdleTimeout必须远大于 10 秒否则会出现「心跳还在跑、连接先被容器回收」的诡异现象日志上看不到任何业务异常只有一条容器关闭记录。反向代理那块Nginx 的典型配置是location /demo/ws/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_read_timeout 300s; # 默认 60s必须调大 proxy_send_timeout 300s; }Upgrade和Connection两行是 WebSocket 协议升级的必需头少一行握手就降级成普通 HTTP客户端报 200 而不是 101。proxy_read_timeout按项目经验至少设成心跳间隔的三倍即 10 秒心跳对应 30 秒以上Demo 里给了 300 秒留足余量。4.3 断线重连退避曲线与离线消息补偿客户端的重连不能是死循环while(true)直接重连那样服务端一重启所有客户端一秒内同时敲门直接把刚起来的实例打挂。标准做法是指数退避加随机抖动let retry 0; const MAX_DELAY 30000; function connect() { const proto location.protocol https: ? wss : ws; const ws new WebSocket(${proto}://${location.host}/demo/ws/order?token${token}); ws.onopen () { retry 0; }; // 连通即重置计数 ws.onclose () { const base Math.min(MAX_DELAY, 1000 * Math.pow(2, retry)); const jitter Math.random() * 1000; // 抖动避免同时重连 setTimeout(connect, base jitter); }; }逻辑说明1000 * 2^retry给出 1s、2s、4s、8s……的上限曲线Math.min封顶 30 秒防止指数爆炸Math.random() * 1000的抖动把同时断开的客户端打散这是防止「重连风暴」的关键一行。参数说明retry在onopen里归零很重要。不归零的话连接稳定一段时间后再次断开重连间隔会从上次的位置继续涨用户体感就是「越用越久才恢复」。服务端这边要配合做离线补偿。Demo 的做法是给每条推送消息带一个自增序号客户端在重连成功的onopen里上报最后收到的序号服务端从缓存里把中间漏掉的补发一次// 重连后客户端上报 lastSeq服务端补发区间内的离线消息 public ListString replay(Long uid, long lastSeq) { DequeSeqMessage buffer offlineBuffer.getOrDefault(uid, new ArrayDeque()); return buffer.stream() .filter(m - m.seq lastSeq) .map(SeqMessage::payload) .collect(Collectors.toList()); }补发缓冲区要有上限Demo 里是 200 条超出就丢弃最旧的并在响应里带上truncatedtrue让客户端知道有缺口、改用拉取接口补数据。无上限的缓冲等于给自己埋一颗内存泄漏的雷。5. 避坑排查五类翻车现场的现象、原因与改法这套代码我前后在几个模拟项目里用过翻车的地方高度集中。下面按「握手阶段」和「连接之后」两段拆开写每条都是现象、原因、解决三段式照着对号入座基本能定位。5.1 握手阶段就被挡回来三条高频现象现象一连接立刻失败浏览器控制台报 403 或Unexpected response code: 403。原因setAllowedOriginPatterns没配或者配的是具体域名但页面实际来源带了端口、大小写不一致。还有一种情况是拦截器里 token 校验失败response.setStatusCode(HttpStatus.UNAUTHORIZED)返回了 401但浏览器统一显示成握手失败。 解决先在浏览器 Network 面板找到那次101请求失败的话是4xx展开看响应头里有没有Sec-WebSocket-Accept。没有说明根本没进握手逻辑把setAllowedOriginPatterns(*)临时放开验证一次确认是跨域问题后再收敛成白名单。如果放开还不行把拦截器里的校验逻辑打日志确认 token 到底传到没有。现象二握手成功返回 101但连接马上被关闭服务端日志没有任何异常。原因走的是Sec-WebSocket-Protocol子协议传 token 的写法服务端没有在响应头里回显同一个子协议名浏览器按协议规范主动断开。或者是 Nginx 转发时丢掉了Upgrade头。 解决确认 Nginx 配置里proxy_set_header Upgrade $http_upgrade;和proxy_set_header Connection upgrade;两行都在。如果是子协议问题在握手响应中显式回写response.getHeaders().set(Sec-WebSocket-Protocol, 客户端声明的子协议名)。现象三本地跑得好好的部署到测试环境后路径 404。原因context-path和网关前缀没拼全。本地是ws://localhost:8080/ws/order测试环境是wss://gateway.example.internal/demo/ws/order多了一段/demo。 解决前端不要硬编码路径用配置项注入前缀比如VITE_WS_PREFIX/demo打包时按环境注入。这个改动很小但能省掉每次换环境的手工排查。5.2 连上了但消息不动两条隐蔽现象现象四单条消息能收到并发推送时随机丢消息服务端日志出现TEXT_PARTIAL_WRITING。原因多个线程同时调用同一个WebSocketSession的sendMessage而它本身不是线程安全的。 解决用ConcurrentWebSocketSessionDecorator包一层再放进注册表所有发送都走装饰后的实例。包装要放在afterConnectionEstablished里做注册表存的必须是包装后的对象否则后面所有发送还是走原会话。现象五单机测试广播正常部署两个实例后用户连在 A 实例上收不到 B 实例产生的消息。原因enableSimpleBroker是纯内存的消息代理两个 JVM 之间的消息完全不互通各自维护各自的订阅关系。 解决两条路。轻量路线是用 Redis 的 pub/sub 做一层广播中继每个实例订阅同一个 channel收到后转发给本地会话重量路线是换外部消息代理如支持 STOMP 的独立 broker并把enableSimpleBroker换成enableStompBrokerRelay。前者的改造成本低得多中小规模够用后者适合订阅关系复杂、需要持久化的场景。选型时先算清楚并发连接数和消息吞吐量别一上来就上重型方案。顺带说一个不算 bug 但很烦的现象开发阶段用localhost连一切正常改成局域网 IP 访问页面后握手报错。这通常是跨域白名单只写了localhost:8080页面来源变成了192.168.x.x:8080属于上面现象一的变种。我一般会在开发环境的配置里直接把白名单写成*生产环境再收敛省得来回改。6. 上线前的压测与验证把 Demo 跑成能扛量的连接服务功能跑通和能扛量之间隔着一次压测。连接数和消息频率是两个独立维度压测时必须分开测先测纯连接保持能力建连后不发言看能挂多久、内存涨多少再测推送吞吐固定连接数逐步加大每秒推送条数看延迟分布。连接保持用wscat做一次小规模冒烟# 建 200 个连接并保持 10 分钟观察服务端内存与连接数 for i in $(seq 1 200); do wscat -c ws://127.0.0.1:8080/demo/ws/order?tokensmoke-token done sleep 600推送吞吐用压测工具自带的长连接采样器参数按这几项配并发连接数 1000、每连接每秒 1 条消息、持续时间 300 秒、超时 15 秒。重点看三个指标——建立连接的成功率低于 99% 说明握手链路有问题、消息端到端 P99 延迟超过 1 秒说明发送线程被阻塞、断连率超过 1% 说明超时参数没对齐。实测下来最容易暴露问题的不是连接数而是发送方式。同步sendMessage在几百连接时看不出问题上千连接后消息延迟会呈阶梯式上升原因是发送线程被慢客户端拖住。这时候要么给每个会话挂独立发送队列要么调大setAsyncSendTimeout并接受少量丢弃。我在模拟项目里踩过这个坑当时的现象是「压测前半段一切正常5 分钟后延迟突然从 200ms 跳到 8 秒」排查了两天才定位到某个客户端网络差导致发送阻塞进而把整个广播线程拖垮。从那以后我每次上线前都强制走一遍「10 个慢客户端混在 1000 个正常连接里」的混合压测专门验证慢客户端会不会拖累全局。还有一项容易被跳过的验证故障恢复。手动 kill 掉一个实例观察客户端多久重连到其他实例以及这期间漏掉的消息能不能补回来。这个场景在演示时永远不会发生但在真实环境里隔三差五就出现。验证方法是把退避曲线和离线补发缓冲区一起看——如果客户端在 30 秒内全部恢复并且消息缺口被补上这套机制就算立住了。希望这些细节能帮你在第一次上线时少走点弯路。本文还有配套的精品资源点击获取
RELATED

相关推荐

VS Code与Cursor编辑器性能对比与选型指南

VS Code与Cursor编辑器性能对比与选型指南

1. 编程编辑器现状与选择困境作为一名在多个技术栈间切换的全栈开发者,我深刻体会到编辑器选择对开发效率的影响。过去五年间,我先后使用过VS Code、JetBrains全家桶、Vim、Emacs以及新锐的Cursor和Zed编辑器。每次切换都伴随着痛苦的适应期,…

📅 2026/10/26 12:26:58
EXIF.js读取失败问题解析与解决方案

EXIF.js读取失败问题解析与解决方案

1. 为什么我们需要关注EXIF.js读取失败问题 上周帮同事排查一个图片管理系统bug时,遇到典型的EXIF读取失败场景:上传的旅游照片在页面上能正常显示,但GPS坐标等元数据始终无法读取。这直接影响了后续的位置标注功能。相信不少前端开发者都遇到…

📅 2026/10/26 12:21:58
主动式智能体如何实现跨端语音上下文理解与动作编排

主动式智能体如何实现跨端语音上下文理解与动作编排

我无法根据当前输入内容生成符合要求的博文。 原因如下: 输入中缺少 项目正文 (即对项目的具体描述、功能说明、技术实现等原始信息); 关键词 字段为空,未提供任何关键词; 摘要描述 字段为空&…

📅 2026/10/26 12:21:58
MORE NEWS

更多资讯

📰

MCP协议:大模型标准化连接与实战应用解析

1. MCP协议:大模型时代的连接器革命 第一次听说MCP(Model Context Protocol)这个概念时,我正被大模型落地应用中的各种接口不兼容问题困扰。就像家里买了一堆智能设备却发现每个都需要不同的插座,大模型在实际业务场景…

📰

2025电商AI趋势:重构人货场关系与跨境新机遇

1. 2025电商行业全景扫描:AI如何重构人货场关系 过去三年全球电商复合增长率达到17.3%,而2025年这个数字预计将突破20%临界点。我在分析300多份行业报告后发现,AI技术渗透率从2022年的12%飙升至2024年的47%,这种指数级增长正在改变…

📰

H.265视频编码技术解析与动漫处理实践

1. 项目概述:dragonballz_e265-1的深度解析 第一次看到"dragonballz_e265-1"这个标题时,我的直觉告诉我这很可能是一个视频编码相关的项目。作为一名长期从事多媒体处理的技术从业者,我见过太多类似命名的文件了。这种命名方式通常…

📰

医疗OCR技术选型与优化实战指南

1. 医疗场景OCR选型的核心挑战 医疗行业的文字识别与其他领域存在显著差异。我在三甲医院信息化建设项目中实测发现,普通OCR模型在医疗单据上的识别准确率平均比商业场景低23.6%。这主要源于三个特征: 特殊版式结构 :检验报告单的表格线密度…

📰

PyTorch运动鞋细粒度图像分类实战:从数据到ONNX部署

做运动鞋识别这个题目,最开始是给一个做电商图像审核的朋友帮忙,他们后台每天要过几万张商品图,人工判断"这双鞋是什么品类、什么系列"效率太低。后来我把这套流程沉淀成了一个完整的实战模板,正好对应学习计划里的第5周…

📰

商务大数据建模实战:从数据清洗到决策优化

1. 项目概述:商务大数据建模与决策实习的核心价值去年夏天,我作为数据分析实习生参与了某大型零售集团的商务大数据项目。当第一次看到"商务大数据建模与决策"这个标题时,我和许多同学一样感到既兴奋又困惑——这究竟意味着什么&am…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬