客户端离线时Java Web服务器WebSocket onclose未触发问题咨询
解决客户端离线时服务器WebSocket
@OnClose 不触发的问题 我来帮你搞定这个问题!当客户端突然离线(比如断网)时,TCP连接会进入半开状态——服务器不会立刻收到断开通知,所以WebSocket的@OnClose事件自然不会触发。要让服务器主动识别并关闭这类无效连接,我们可以通过「心跳检测+超时机制」来实现,下面给你两种具体方案:
方案一:利用Tomcat内置的WebSocket超时配置
Tomcat本身提供了WebSocket会话的超时配置,你只需要在项目的web.xml或者Tomcat全局的conf/web.xml中添加以下参数,就能让服务器自动关闭空闲超时的WebSocket连接:
<!-- 设置WebSocket会话空闲超时时间,单位毫秒,这里配置为30秒 --> <context-param> <param-name>org.apache.tomcat.websocket.sessionTimeout</param-name> <param-value>30000</param-value> </context-param>
当WebSocket会话超过30秒没有任何数据交互时,Tomcat会主动关闭连接,此时服务器的@OnClose方法就会被触发。这种方式无需修改业务代码,适合快速解决问题。
方案二:自定义心跳检测逻辑(更灵活可控)
如果需要和客户端的ping/pong逻辑联动,实现更精准的离线检测,可以自定义心跳超时机制:
1. 优化客户端的ping发送逻辑
你的客户端已经有每5秒发送ping的代码,我们可以完善一下,处理发送失败的情况并清理定时器:
var ws; var wsUrl = 'wss://' + location.host + '/test-websocket'; var pingInterval; window.onload = function () { createWebSocket(wsUrl); }; window.onbeforeunload = function () { if (ws) ws.close(1000, "Client closed normally"); }; function createWebSocket(url) { ws = new WebSocket(url); initEventHandle(); } function initEventHandle() { ws.onerror = function () { console.log("WebSocket error occurred"); clearInterval(pingInterval); }; ws.onclose = function (event) { console.log(`WebSocket closed: ${event.code} - ${event.reason}`); clearInterval(pingInterval); }; ws.onopen = function () { console.log("WebSocket connected successfully"); // 每5秒发送一次ping,仅在连接正常时发送 pingInterval = setInterval(function () { if (ws.readyState === WebSocket.OPEN) { var message = { id: "ping" }; sendMessage(message); } }, 5000); }; ws.onmessage = function (message) { console.info('Received message: ' + message.data); } } function sendMessage(message) { try { const jsonMessage = JSON.stringify(message); console.log('Sending message: ' + jsonMessage); ws.send(jsonMessage); } catch (e) { console.error("Failed to send message:", e); clearInterval(pingInterval); } }
2. 服务器端添加心跳超时检测
在WebSocket端点类中,维护每个会话的最后活跃时间,定时检查超时会话并主动关闭:
import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; import javax.websocket.*; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @ServerEndpoint("/test-websocket") public class TestWebSocketEndpoint { private static final Gson GSON = new GsonBuilder().create(); // 存储所有活跃会话和对应最后活跃时间 private static final Map<Session, Long> SESSION_LAST_ACTIVE = new ConcurrentHashMap<>(); // 超时阈值:15秒(客户端每5秒发ping,超过3个周期无响应则判定离线) private static final long TIMEOUT_MS = 15000; // 定时任务池,用于周期性检查超时会话 private static final ScheduledExecutorService SCHEDULER = Executors.newSingleThreadScheduledExecutor(); static { // 每10秒执行一次超时检查 SCHEDULER.scheduleAtFixedRate(() -> { long currentTime = System.currentTimeMillis(); for (Map.Entry<Session, Long> entry : SESSION_LAST_ACTIVE.entrySet()) { Session session = entry.getKey(); // 检查是否超时 if (currentTime - entry.getValue() > TIMEOUT_MS) { try { // 主动关闭会话,触发@OnClose session.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "Session timeout due to inactivity")); } catch (IOException e) { e.printStackTrace(); } } } }, 0, 10, TimeUnit.SECONDS); } @OnOpen public void onOpen(Session session) { // 会话建立时记录初始活跃时间 SESSION_LAST_ACTIVE.put(session, System.currentTimeMillis()); System.out.println("New session opened: " + session.getId()); } @OnMessage public void onMessage(String message, final Session session) throws IOException { JsonObject jsonMessage = GSON.fromJson(message, JsonObject.class); System.out.println("Received message: " + jsonMessage); // 收到消息(包括ping)时更新活跃时间 SESSION_LAST_ACTIVE.put(session, System.currentTimeMillis()); if ("ping".equals(jsonMessage.get("id").getAsString())) { session.getBasicRemote().sendText("pong"); } } @OnError public void onError(Session session, Throwable error) { error.printStackTrace(); // 出错时移除会话记录 SESSION_LAST_ACTIVE.remove(session); } @OnClose public void onClose(Session session, CloseReason reason) { System.out.println("Session closed: " + session.getId() + ", reason: " + reason.toString()); // 会话关闭时移除记录 SESSION_LAST_ACTIVE.remove(session); } }
为什么之前的代码没触发@OnClose?
当客户端突然断网,TCP连接没有发送正常的FIN断开包,服务器会一直认为连接是有效的(半开连接)。只有当服务器尝试向该连接发送数据时,才会收到TCP的RST包,此时才会触发关闭事件,但这个过程可能需要几分钟(取决于系统TCP超时配置)。所以必须通过主动的心跳检测来提前识别无效连接。
内容的提问来源于stack exchange,提问作者EmmHaha
相关产品推荐
相关产品推荐

