You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

客户端离线时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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 03:52:04