news 2026/10/2 4:52:07

Java WebSocket从零到生产:实战心跳机制与高并发连接管理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java WebSocket从零到生产:实战心跳机制与高并发连接管理

1. 为什么是WebSocket:当你需要"服务器主动找上门"时

在Java Web开发里摸爬滚打几年的人,大概率都经历过这样一个阶段:接到一个"实时推送"需求,第一反应是轮询,第二反应是长轮询,第三反应才是WebSocket。我刚开始接触这块时也是这样,直到在一个在线客服系统里被HTTP轮询折磨得够呛——客户端每3秒发一次请求,服务器明明没数据,也得回一个空响应,数据库连接被白白消耗,网络流量也翻了好几倍。

1.1 从轮询到长连接:HTTP的心累,WebSocket的轻松

先看看最原始的短轮询是怎么工作的:浏览器定时器每隔几秒发一个Ajax请求,问服务器"有新消息吗",没有就返回空。这种方式逻辑简单,但问题非常明显——大部分请求都是无效的、浪费的。即便优化成连接合并、批量查询,本质上还是"客户端反复敲服务器门,只为了看看有没有信"。

长轮询稍微聪明一点:客户端发起请求后,服务器先hold住这个连接,等到真有数据了再响应。这样请求次数少了,但服务器端需要维护大量挂起的HTTP请求,一旦超时又要重连,连接对象堆积得多了,内存和线程开销非常可观。

WebSocket的思路完全不同。它把"问一句答一句"改成了"打通一个双向管道"。握手只发生一次,之后服务器和客户端可以随时互相发数据,数据帧只有几字节的开销,没有HTTP头部的重复传输。用打电话类比:轮询是每隔几分钟给对方发一条"你说话了吗"的短信,而WebSocket是直接打通电话,两边随时讲话随时听。

从Java的角度看,JDK标准里没有内置WebSocket客户端/服务端实现,但Java EE的JSR 356规范(javax.websocket)已经定义得明明白白,Tomcat、Jetty、Undertow这些主流容器都实现了它。Spring Boot也在此基础上提供了很好的封装。这就是我这个Java程序员今天能在这里写一篇"零基础到精通"长文的地基。

1.2 协议核心:一次握手,一个帧

WebSocket协议的原理,其实只看两个关键动作就够了:握手和数据帧。

握手这一步基于HTTP升级机制:客户端发起一个普通GET请求,头上带着Upgrade: websocket、Connection: Upgrade、Sec-WebSocket-Key等字段。服务器验证通过后,返回101 Switching Protocols响应,连接协议就正式切换成WebSocket。这里有个细节值得注意——Sec-WebSocket-Key不是用来鉴权的,它只是一串随机数,服务器按约定拼上固定GUID做SHA1并Base64返回,就是为了证明"对面真的是WebSocket服务器"。真正的身份校验,得靠URL里的token或握手阶段的自定义Header。

数据帧的结构也不复杂:一个FIN标记位表示这是不是最后一帧,opcode表示帧类型(1是文本,2是二进制,8是关闭连接,9是Ping,10是Pong),然后是长度字段和掩码标记。有个容易踩坑的点——浏览器发到服务器的帧必须做掩码处理,服务器发到浏览器的帧不需要掩码。这部分协议底层由容器实现了,但对理解"为什么有的抓包工具看不到消息内容"很有帮助。

零基础的人不用纠结帧的二进制细节,但必须理解一个关键推论:WebSocket的连接是长久的、有状态的,所以服务器端必须自己维护每个连接会话,并且处理连接失效的情况。这也是后面心跳机制和排错章节的铺垫。

2. 从零搭建Java WebSocket服务端:原生API跑通全过程

我见过很多新手学WebSocket,上来就配Spring的STOMP,配RabbitMQ,搞得非常复杂。其实入门阶段完全不需要这些,用Java原生注解API就能在半小时内跑通一个实时聊天Demo。等你理解了连接生命周期,再往Spring生态、集群方向扩展,会顺畅得多。

2.1 依赖引入与项目结构

如果你的项目是Spring Boot环境,开发期最省事的做法是引入spring-boot-starter-websocket,它会带上Tomcat的WebSocket实现。如果是不依赖Spring的纯Servlet项目,直接在pom里加上Tomcat的websocket依赖即可。

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>

项目结构上,我建议按"端点类+配置类+工具类"来拆。端点类负责业务回调,配置类负责注册,工具类负责管理Session。千万不要把几百行逻辑全塞进一个类里,后面维护会很难受。

2.2 @ServerEndpoint注解:一小时上手的核心用法

在Java原生API里,定义一个WebSocket服务端端点,只需要在上面的注解里声明路径和回调方法。下面的例子是单个连接的收发回声:

import javax.websocket.*; import javax.websocket.server.ServerEndpoint; import java.io.IOException; @ServerEndpoint("/echo") public class EchoEndpoint { @OnOpen public void onOpen(Session session) { System.out.println("连接打开,session id = " + session.getId()); } @OnMessage public void onMessage(String message, Session session) throws IOException { System.out.println("收到消息:" + message); session.getBasicRemote().sendText("回声:" + message); } @OnError public void onError(Session session, Throwable error) { error.printStackTrace(); } @OnClose public void onClose(Session session, CloseReason reason) { System.out.println("连接关闭,原因 = " + reason.getReasonPhrase()); } }

在Spring Boot中使用时,有个我见过无数人踩过的坑:直接写@ServerEndpoint的类默认不会被注入Spring容器管理。如果你在端点类里想注入Service,会得到一个空指针。必须在配置类里注册一个ServerEndpointExporter的Bean,让它自动发现并注册所有@ServerEndpoint注解的类,并且在该类上加上@Component,这样Spring才能帮你完成依赖注入。

import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }

注意,如果项目是打War包部署到外部Tomcat的,ServerEndpointExporter反而可能会冲突。此时不要加它,直接依赖Tomcat的类扫描。这个差异官方文档提过,但很容易被忽略,我当年在这个问题上卡了将近一下午。

2.3 连接生命周期:打开、消息、错误、关闭

上面四个注解对应WebSocket连接的四个阶段,理解它们比记住代码重要。

@OnOpen是握手成功后服务器执行的回调。这个阶段最适合做两件事:保存Session到全局Map、给客户端发一条欢迎消息。Session是服务器和这个客户端的唯一通道,它的生命周期等于连接生命周期,所以必须妥善保存。

@OnMessage是业务逻辑的核心入口。注意,这里的方法参数可以是String、byte[]、Reader、InputStream,甚至自定义对象(通过decoder处理)。参数里可以注入Session,也可以单独注入。如果消息处理抛异常,会走@OnError而不是直接断开,这是我实际调试中发现的一个细节——很多人以为错误会导致立即关闭,其实容器会先回调onError,再根据情况决定是否关闭连接。

@OnClose负责清理资源。这里最常见的错误是忘了从Map里移除Session,导致"僵尸连接"越积越多,内存泄漏。

@OnError要特别注意一个点——出错之后最好主动调用session.close(),否则连接状态不明确,客户端那边可能一直以为还连着。

2.4 多人在线广播:SessionMap的设计

单回声Demo没多大实际意义,真实项目里更常见的是"一个用户上线,所有在线用户都能收到通知"。这需要用静态Map维护全部活跃Session,再遍历发送。

import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @ServerEndpoint("/chat/{username}") public class ChatEndpoint { private static final Map<String, Session> ONLINE_USERS = new ConcurrentHashMap<>(); @OnOpen public void onOpen(Session session, @PathParam("username") String username) { ONLINE_USERS.put(username, session); broadcast("用户 " + username + " 上线了,当前在线:" + ONLINE_USERS.size()); } @OnMessage public void onMessage(String message, Session session, @PathParam("username") String username) { broadcast(username + ":" + message); } @OnClose public void onClose(Session session, @PathParam("username") String username) { ONLINE_USERS.remove(username); broadcast("用户 " + username + " 下线了,当前在线:" + ONLINE_USERS.size()); } @OnError public void onError(Session session, Throwable error) { error.printStackTrace(); } private void broadcast(String message) { for (Map.Entry<String, Session> entry : ONLINE_USERS.entrySet()) { try { entry.getValue().getBasicRemote().sendText(message); } catch (IOException e) { e.printStackTrace(); } } } }

这里有个并发安全细节:消息发送应该用session.getBasicRemote()还是session.getAsyncRemote()?单条小消息用Basic没问题,但它是同步阻塞的,如果一条消息发送缓慢,会阻塞当前线程。高并发场景建议用AsyncRemote(异步发送),每条消息几十毫秒内就返回发送结果,真正的I/O由容器线程池处理。如果是大量广播,还可以配合BatchedMessage批量发送减少系统调用次数。

3. 心跳机制不写,三天后连接就断:超时检测的设计思路

热词里"websocket心跳机制实现"排得很靠前,说明这个问题困扰了很多人。我敢说大多数Java开发者写过WebSocket Demo,但很少有人在项目上线前认真设计心跳机制,直到生产环境频繁掉线才回头补课。

3.1 为什么必须有心跳:网络设备不认"静默连接"

WebSocket连接虽然建立后是长活着的,但它本质上是一条TCP连接。TCP连接有超时机制,更重要的是,中间经过的负载均衡器、防火墙、运营商网络设备,都会清理"空闲太久的连接"。Nginx的proxy_read_timeout默认60秒,也就是说,如果60秒内后端或客户端没有任何数据传输,Nginx可能主动切断连接。

在实际项目里,用户可能只是把页面挂着不动,几分钟甚至半小时都不操作。这时服务器和浏览器之间的数据通路早就被中间设备悄悄掐断了,但双方都没有立刻感知到。等服务器想推送消息时,才发现连接已经死了;或者客户端想发消息,已经发不出去了。这就是所谓"连接但不接收信息"的一大元凶。

心跳机制就是为了对抗这种问题:每隔一段时间主动发一个"我还活着"的信号,让连接保持活跃,同时探测连接是否真的可用。

3.2 协议级Ping/Pong vs 业务级心跳

WebSocket协议本身定义了Ping帧和Pong帧。浏览器端的WebSocket API没法直接发协议级Ping帧,但服务器端可以通过容器的底层API发。比如Tomcat的WsSession暴露了sendPingMessage方法,Jetty也有对应API。

不过,在实际Java项目里,我见过更多的方案是"业务级心跳":约定一个特殊的消息格式,比如客户端每隔30秒发一个文本帧{"type":"ping"},服务器收到后回复{"type":"pong"}。这种方案的好处是跟语言无关、协议无关,前端JS处理后端Java解析都简单,而且可以在心跳包里携带额外信息(比如客户端当前在线状态、唯一标识)。

还有一种更省事的方案:只依赖服务器端的空闲超时检测,不给客户端发送任何东西。也就是定期扫描那些"超过N秒没有任何消息"的连接,把它们强制关闭。这样服务器端能清理僵尸连接,但解决不了"中间设备已经切断连接而服务器不知情"的问题——因为服务器根本不会主动发数据。

我的建议是两层一起做:

  • 客户端每隔30秒发送一次业务心跳消息。
  • 服务器端维护每个Session的最后心跳时间,超过60秒未收到任何消息,就判定超时,主动关闭连接。
  • 服务器端如果有自己的空闲连接清理线程(比如容器级别的maxIdleTimeout),也一起配置,兜底。

3.3 服务端超时检测的具体实现

Java的javax.websocket API里,Session有个setMaxIdleTimeout(long)方法,单位毫秒,设置为0表示永不超时。这个方法属于容器级别的空闲超时,只要有数据帧经过就会刷新计时。用它可以做一个基础防线,但它不区分消息类型,业务上还需要更精细的控制。

下面是一个简单的服务端心跳扫描实现:

import javax.websocket.Session; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class HeartbeatManager { private static final Map<String, Session> SESSION_MAP = new ConcurrentHashMap<>(); private static final Map<String, Long> LAST_HEARTBEAT = new ConcurrentHashMap<>(); private static final ScheduledExecutorService SCHEDULER = Executors.newSingleThreadScheduledExecutor(); public static void start() { SCHEDULER.scheduleAtFixedRate(() -> { long now = System.currentTimeMillis(); for (Map.Entry<String, Long> entry : LAST_HEARTBEAT.entrySet()) { if (now - entry.getValue() > 60_000) { Session session = SESSION_MAP.get(entry.getKey()); if (session != null && session.isOpen()) { try { session.close(); System.out.println("心跳超时关闭连接:" + entry.getKey()); } catch (Exception e) { e.printStackTrace(); } SESSION_MAP.remove(entry.getKey()); LAST_HEARTBEAT.remove(entry.getKey()); } } } }, 10, 10, TimeUnit.SECONDS); } public static void register(Session session, String userId) { SESSION_MAP.put(userId, session); LAST_HEARTBEAT.put(userId, System.currentTimeMillis()); } public static void heartbeat(String userId) { LAST_HEARTBEAT.put(userId, System.currentTimeMillis()); } }

这里的细节在于扫描间隔和超时阈值的关系。如果客户端30秒发一次心跳,服务端60秒判定超时,扫描线程10秒跑一次,就能容忍一次心跳丢失,但要连续丢两次才判死,容错比较合理。如果你把超时时间设成和心跳间隔一样短,网络稍微抖动一下就会误杀连接,体验会非常差。

4. "连接但不接收信息":我排查过的三个真实案例

热词里有个"websocket连接但不接受信息"(更精确的说法是"连接建立了,但收不到消息"),这是我在社区被问得最多的一类问题。整理一下我自己遇到的和帮别人解决的三个典型场景,每个都展现了完全不同的根因。

4.1 案例一:消息推给了旧Session

现象是:用户A刷新了页面,重新建立了WebSocket连接,但服务器往旧连接上推送消息,前端当然收不到。

排查过程:

  • 先看服务端日志,确认消息确实发出了,且没有抛异常。
  • 再在Chrome DevTools的Network面板里找到WebSocket连接,看它的Message状态。如果连接显示已关闭,说明推送目标是历史连接。
  • 查看SessionMap的注册和清理逻辑,发现用户A重连时,新的Session覆盖了Map里的旧值,但旧连接对象还留在别的地方;或者广播时遍历的是另一份Map,两边数据不一致。

根因其实是Session生命周期管理不严格。解决方案也简单:Map的key不要用容易重复的用户ID,而是用Session.getId();或者在@OnOpen里先做一次removePrevious(userId),把同一个用户旧的Session主动关闭再注册新的。更彻底的做法是用SessionListener监听容器内Session的销毁事件,统一清理。

4.2 案例二:服务端抛异常,客户端却毫无感知

另一个典型场景是:前端连上了,也显示"WebSocket已连接",但消息就是不来。服务端日志里也没有明显的业务错误。

排查过程:

  • 拿起Chrome DevTools的Console和Network翻了一遍,发现WebSocket的帧里什么也没有。
  • 改用抓包工具wscat直接连服务器,发同样的消息,居然能收到回复。这说明问题出在前端和服务端的某个中间环节。
  • 翻服务端日志,发现一条异常栈:JSON序列化报错,@OnMessage方法抛了JsonProcessingException。

这里要说明一下:javax.websocket的@OnMessage方法如果抛异常,容器会调用@OnError回调,但不会中断其他连接。问题在于,如果@OnError里只是打日志没关闭连接,客户端那边的连接看起来还是好的,但它永远等不到任何数据。而且如果异常发生在消息解码阶段(decoder),客户端可能连错误提示都没有,直接表现为"静默丢失"。

以后遇到"连接正常但收不到消息"的情况,第一件事永远是在服务端打开日志看有没有异常。尤其是检查decoder/encoder类,看看是不是消息对象转JSON时遇到了字段序列化问题,或者编码器没有在@ServerEndpoint注解里声明。

4.3 案例三:负载均衡器掐断了空闲连接

这个案例很有代表性。某个生产环境的WebSocket服务挂在Nginx后面,没配心跳,用户挂机超过60秒就收不到推送了。从浏览器看,连接状态仍然是OPEN,但服务器发消息,前端收不到,直到下次刷新页面才恢复正常。

排查过程:

  • 先看Nginx的配置,proxy_read_timeout是默认的60s,proxy_send_timeout也是60s。
  • 再看TCP连接状态,发现服务端和Nginx之间的连接变成CLOSE_WAIT,而Nginx到浏览器的连接还保持,说明Nginx已经主动断了后端连接。
  • 复现步骤很清晰:连上WebSocket,什么都不做,等60秒,尝试服务器推送,失败。

这种问题的正确解法是双向的:

  • Nginx配置里把proxy_read_timeout和proxy_send_timeout调大,比如3600s。
  • 同时客户端加心跳,保证每30秒有一次数据传输,让Nginx的计时器不断刷新。
  • 另外proxy_http_version要改成1.1,并且设置Upgrade相关的Header,否则WebSocket的升级请求会被Nginx拒绝。

我自己在写配置时踩过一个坑:光调了proxy_read_timeout,忘记了proxy_send_timeout,从服务器往浏览器推数据超过该时间没响应,一样被断。两个参数必须同时调整。

5. 生产环境进阶:并发推送、集群部署和前端配合

跑通了本地Demo,不等于能上线。生产环境要考虑并发量、多实例部署、前端断线重连,每一块都是单独的深水区。

5.1 会话管理:别再单机Map一把梭

单机部署时,一个静态ConcurrentHashMap确实够用。但并发量上来以后有几个隐形问题。

第一,Session对象是不是线程安全的?javax.websocket的Session不是严格线程安全的,不能多个线程同时对同一个Session调用sendText,否则可能出现数据交错或IllegalStateException。解决方式是每个Session配一个单线程执行器,把发给同一条连接的N条消息串行化。Spring的ConcurrentWebSocketSessionDecorator做了类似的事,可以借鉴。

第二,Map的value要小心空引用和腐坏连接。我见过有人在遍历Map发送时,不检查session.isOpen(),结果消息发到已关闭的连接上,抛IOException后还把整个循环中断了。正确写法是发送前检查isOpen,发送失败后从Map移除。

第三,如果是高并发推送,可以考虑批量发送模式。Tomcat的WsSession支持通过BatchMode批量缓冲帧,减少I/O次数,对大量小消息很有效果。但使用批量模式时要记得flushBatch(),否则消息可能一直积压到连接关闭才发出去。

5.2 集群部署:消息怎么跨节点路由

当服务从1台变成N台时,问题就来了:用户A连的是节点1,用户B连的是节点2,节点1收到A的消息,怎么推给B?

答案是把"广播"从进程内调用,改成"消息中间件广播"。常见的方案:

  • Redis的Pub/Sub。实现简单,基于发布订阅模型,消息不落库,适合WebSocket实时广播这种"发了就不管"的场景。
  • RabbitMQ/Kafka这类MQ。功能更强,可以做持久化、延时队列,但架构更重。
  • 如果不想引入外部组件,还可以用多个节点之间的内部HTTP通道互相转发,但复杂度和可用性都不如消息中间件。

用Redis Pub/Sub的核心代码思路是:每个节点的WebSocket服务,收到业务消息后,第一件事不是直接推给本机Session,而是先发布到Redis频道;同时每个节点订阅同一个频道,收到别的节点发布的消息后,从本机SessionMap里找到目标用户推送。

// 伪代码:节点收到消息后的处理 @OnMessage public void onMessage(String message, Session session, @PathParam("userId") String userId) { // 1. 本地推送 sendToLocalUser(userId, message); // 2. 广播到Redis,让其他节点也推送 redisTemplate.convertAndSend("websocket:broadcast", userId + "|" + message); } // Redis订阅回调 public void onRedisMessage(String payload) { String[] parts = payload.split("\\|"); sendToLocalUser(parts[0], parts[1]); }

这里有个必须警惕的坑:如果消息只是给单个用户推送,就不应该用上面的"广播所有节点"方案,否则每个节点都会收到并尝试推送,很容易造成重复推送。正确的做法是带上目标节点标识,或者用Redis的channel命名做路由,比如websocket:node1这样的频道,每个节点只订阅自己的频道。或者更简单,用Redis的List/Stream先定位用户Session落在哪个节点,再定向推送。

5.3 前端JS怎么配合:重连策略与心跳定时器

热词里"websocket js"和"react + sse/websocket 轮询文件变化"搜索量不低,说明很多问题发生在前后端配合上。Java后端写得再稳,前端如果不考虑断线重连,体验也会稀碎。

前端核心代码其实很短,但有几个容易忽略的细节。

function connectWebSocket() { const ws = new WebSocket('ws://localhost:8080/ws?token=xxx'); ws.onopen = () => { console.log('WebSocket已连接'); // 开启心跳 heartbeatTimer = setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'ping' })); } }, 30000); }; ws.onmessage = (event) => { // 收到消息,重置重连次数 reconnectAttempts = 0; console.log(event.data); }; ws.onclose = () => { clearInterval(heartbeatTimer); // 指数退避重连 const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000); setTimeout(connectWebSocket, delay); reconnectAttempts++; }; ws.onerror = () => { // onerror之后通常跟着onclose,所以重连逻辑放onclose里 ws.close(); }; }

两个注意点:一是onerror里不要重复调用connectWebSocket,因为onclose几乎一定会触发,双重重连会让连接数翻倍。二是重连次数一定要重置,否则退避时间会一直指数增长,最后卡在30秒上限不动。前端还可以在URL参数里带上当前用户ID和token,服务端在@OnOpen里解析并发给客户端一个握手确认消息,这样能减少重连时的身份校验逻辑。

5.4 面试高频题:WebSocket与HTTP的区别

既然热词里有"java面试题"和"java开发工程师面试题",顺便把几个常考点梳理一遍,面试时不会慌。

  • WebSocket和HTTP是什么关系:WebSocket握手阶段完全走HTTP协议,升级后变成独立的TCP长连接协议。
  • 连接建立后,谁可以主动发消息:双方都可以。HTTP只有客户端能主动请求。
  • 为什么WebSocket省流量:数据帧头只有几个字节,而HTTP请求头动辄几百字节。
  • 什么时候用SSE、什么时候用WebSocket:单工推送、服务器到浏览器方向为主的场景,SSE更简单;双向交互实时性要求高,选WebSocket。
  • 如何保证消息顺序:WebSocket本身是有序的(基于TCP),但多线程并发发送时同一个Session可能乱序,需要配合单线程执行器。
  • WebSocket和TCP的关系:WebSocket是应用层协议,TCP是传输层协议,WebSocket帧的可靠性由TCP保证。

6. 记住这些就够了:会话清理、日志埋点和验收清单

最后聊几个我实际操作中的习惯,这部分不算高深理论,但能省掉很多线上事故。

6.1 会话清理的兜底方案

SessionMap的清理不能只靠@OnClose。原因很简单:客户端断网、断电、进程被杀的时候,服务器可能根本收不到关闭帧,TCP半开连接也不会主动通知应用层。兜底方案就是在之前的HeartbeatManager里做定期扫描,凡是超过阈值未通信的Session一律关闭删除,别指望客户端"自觉"。

另外提醒一句:传统的ConcurrentHashMap是好的,但如果你自己写了一个"带过期时间的Map",记得考虑弱引用和防内存泄漏。我见过有人用普通的HashMap,在并发环境下扩容导致死循环,直接CPU飙到100%。

6.2 日志埋点和问题定位

WebSocket的问题是出了名的"难抓现场",因为连接一旦断开,信息就丢了。我的做法是:

  • 每个Session建立时,打一条带Session ID、用户ID、来源IP的日志。
  • 每次收发消息,打一条带方向和消息长度的日志(内容级别按需,生产环境不建议打全量消息体)。
  • 连接关闭时,打一条带关闭原因码的日志。
  • 全局再挂一个SessionListener,把所有打开/关闭事件汇总到一个监控指标里。

有了这些日志,再遇到"收到消息但前端没反应",直接按Session ID查链路,很快就能定位是服务端没发出,还是发出但前端没收到,还是中间网络断了。

6.3 上线前自检清单

我每次给新项目接WebSocket前,都会过一遍下面的清单:

  • 握手URL有没有带鉴权参数?服务端是否校验了token?
  • SessionMap有没有清理逻辑?@OnClose里是否移除了对应条目?
  • 心跳机制是否已实现?客户端和服务端的超时时间是否匹配?
  • Nginx或网关的read/send超时和升级Header是否配置正确?
  • 是否限制了单用户的并发连接数?防止同一个账号被重复登录刷爆SessionMap?
  • 是否配置了最大消息大小?Tomcat默认单帧不能超过8KB(maxTextMessageBufferSize),要传大文件或长文本得显式调大。
  • 跨域问题:前端域名和后端不一致时,allowed-origins要显式配置,否则浏览器会拦截握手响应。
  • 压测过没有?至少用几万条消息跑一遍,看内存和GC有没有明显异常。

这些检查项看着零碎,但每一条背后都有过真实的事故案例。WebSocket作为一种长连接协议,它的坑不在"建立连接",而在"连接的生命周期管理"——什么时候该关、什么时候该踢、中间设备会不会悄悄掐断。把这些想清楚了,你的Java WebSocket技能就从"能跑Demo"升级成"能抗生产"了。

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

Unity双端动态换图标实战:Android activity-alias与iOS备用图标方案

手游上线后想换个图标做活动&#xff0c;结果发现应用商店的图标是打包时写死的&#xff0c;改一次就得重新提审、重新发版&#xff0c;等审核通过活动热度都过了。这个痛点做发行的朋友应该都懂。动态换图标这个需求&#xff0c;最早是iOS端先火起来的&#xff0c;后来Android…

作者头像 李华
网站建设 2026/10/2 4:51:23

汇川Easy301与MCGS通过Modbus RTU实现浮点数通讯详解

1. 项目概述&#xff1a;为什么这个通讯组合在产线调试中让人又爱又恨&#xff1f;汇川Easy 301 PLC和MCGS触摸屏通过RS-485走Modbus RTU协议做浮点数读写&#xff0c;这事儿听起来平平无奇&#xff0c;但真上手调通的那一刻&#xff0c;我盯着MCGS画面上跳动的温度值、压力值、…

作者头像 李华
网站建设 2026/10/2 4:51:14

双站测角定位中GDOP的原理、计算与布站优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 4:51:14

LangGraph多智能体工程实践:状态设计与工具调用的关键要点

LangGraph 做多智能体&#xff0c;最容易被忽略的其实是工程那一层。网上教程大多停在怎么画图、怎么把两个 agent 串起来&#xff0c;可一放到生产环境&#xff0c;状态管理、工具调用、超时恢复、并发隔离这些问题一个接一个冒出来。这篇文章不重复概念&#xff0c;我直接整理…

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

组态王直连MCGS触摸屏:Modbus TCP通讯配置与排查

1. 一个被问烂了的问题&#xff1a;组态王到底能不能直接读MCGS触屏的数据先说结论&#xff0c;能&#xff0c;而且不复杂&#xff0c;前提是你得接受"两边通过 Modbus TCP 握手"这个事实。很多人在网上一搜"组态王 通讯 MCGS"&#xff0c;跳出来的全是各说…

作者头像 李华
网站建设 2026/10/2 4:51:06

微信开源知识库深度拆解:RAG架构与私有化问答实战

微信开源了一个知识库项目&#xff0c;准确说是把整套知识库底座直接开源了。这个项目不是那种包装成“知识库”的演示 Demo&#xff0c;而是能把散落在 PDF、Word、网页、扫描件甚至微信聊天记录里的内容&#xff0c;统一解析、索引、向量化&#xff0c;最后接上大模型做私有化…

作者头像 李华