WebSocket双端心跳保活机制完整实现方案(Java+JavaScript)

WebSocket双端心跳保活机制完整实现方案

一、为什么需要心跳机制?

  1. 防止中间设备(NAT/防火墙)断开「空闲连接」
  2. 及时检测断线情况(网络波动、客户端崩溃)
  3. 保持双端连接状态同步
  4. 避免僵尸连接占用服务资源
    心跳机制示例图

二、完整技术方案

2.1 双端心跳设计

🔁 双向检测机制

  • 协议层Ping/Pong(TCP保活)
  • 应用层Heartbeat/Ack(业务保活)
  • 双重保障应对不同网络环境

超时控制策略

参数类型默认值说明
心跳间隔30s两次心跳的间隔时间
心跳超时90s连续3次未响应视为失效
最大重试次数3次前端重连最大尝试次数

2.2 后端Java实现

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
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.ScheduledFuture;
import java.util.concurrent.TimeUnit;

@ServerEndpoint("/ws/exp/{userId}")
@Component
public class ExpServer {
    
    // 心跳间隔时间(秒)
    private static final long HEARTBEAT_INTERVAL = 30;
    // 心跳超时时间(秒)
    private static final long HEARTBEAT_TIMEOUT = 90;
    
    // 线程池(静态共享)
    private static final ScheduledExecutorService scheduler = 
        Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors());
    
    // 保存所有连接的心跳任务
    private static final Map<String, ScheduledFuture<?>> heartbeatTasks = 
        new ConcurrentHashMap<>();
    
    private Session session;
    private String userId;
    private volatile long lastHeartbeatTime = System.currentTimeMillis();

    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) {
        this.session = session;
        this.userId = userId;
        this.lastHeartbeatTime = System.currentTimeMillis();
        
        // 配置连接参数
        session.setMaxIdleTimeout(HEARTBEAT_TIMEOUT * 1000);
        
        // 启动心跳任务
        ScheduledFuture<?> task = scheduler.scheduleAtFixedRate(() -> {
            try {
                checkHeartbeat();
                sendPing();
            } catch (Exception e) {
                closeSession();
            }
        }, HEARTBEAT_INTERVAL, HEARTBEAT_INTERVAL, TimeUnit.SECONDS);
        
        heartbeatTasks.put(userId, task);
    }

    @OnClose
    public void onClose() {
        closeSession();
    }

    @OnError
    public void onError(Throwable error) {
        closeSession();
    }

    @OnMessage
    public void onMessage(String message) {
        // 处理应用层心跳
        if ("HEARTBEAT".equals(message)) {
            sendMessage("HEARTBEAT_ACK");
            updateHeartbeatTime();
        }
    }

    // 发送协议层Ping
    private void sendPing() {
        if (session.isOpen()) {
            try {
                session.getBasicRemote().sendPing(null);
            } catch (IOException | IllegalArgumentException e) {
                closeSession();
            }
        }
    }

    // 检查心跳超时
    private void checkHeartbeat() {
        long current = System.currentTimeMillis();
        if (current - lastHeartbeatTime > HEARTBEAT_TIMEOUT * 1000) {
            closeSession();
        }
    }

    // 更新心跳时间(收到Pong或应用层心跳)
    public void updateHeartbeatTime() {
        lastHeartbeatTime = System.currentTimeMillis();
    }

    // 安全关闭连接
    private synchronized void closeSession() {
        try {
            if (session != null && session.isOpen()) {
                session.close();
            }
        } catch (IOException e) {
            // 日志记录
        } finally {
            ScheduledFuture<?> task = heartbeatTasks.remove(userId);
            if (task != null) {
                task.cancel(true);
            }
        }
    }

    private void sendMessage(String message) {
        if (session.isOpen()) {
            try {
                session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                closeSession();
            }
        }
    }
}
关键代码说明:
  1. 线程池配置

    private static final ScheduledExecutorService scheduler = 
        Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors());
    
    • 使用独立线程池处理心跳任务
    • 根据CPU核心数动态分配线程
  2. 双心跳触发逻辑

    // 协议层Ping
    session.getBasicRemote().sendPing(null); 
    
    // 应用层心跳检测
    if ("HEARTBEAT".equals(message)) {
        sendMessage("HEARTBEAT_ACK");
    }
    
  3. 连接关闭处理

    private synchronized void closeSession() {
        // 关闭WebSocket连接
        session.close(); 
        // 取消定时任务
        task.cancel(true); 
        // 清理资源
        heartbeatTasks.remove(userId); 
    }
    

2.3 前端JavaScript实现

class WebSocketHeartbeat {
  constructor(url) {
    this.ws = new WebSocket(url);
    this.ws.onpong = () => this.resetTimeout();
    
    // 双心跳检测
    setInterval(() => {
      this.ws.send('HEARTBEAT');  // 应用层心跳
      this.ws.ping();            // 协议层心跳
    }, 30000);
  }
  
  // 完整实现参见下文
}
核心功能解析:
// 心跳超时处理
setTimeout(() => {
  console.warn('心跳超时,主动断开');
  this.ws.close();
}, 90000);

// 自动重连机制
reconnect() {
  if (this.retryCount < 3) {
    setTimeout(() => {
      new WebSocketHeartbeat(url); 
    }, 5000 * this.retryCount);
  }
}

三、部署注意事项

  • 需要处理线程安全问题(使用ConcurrentHashMap)
  • 生产环境建议添加日志记录
  • 前端需要处理自动重连逻辑
  • 建议结合Nginx配置调整超时参数:
    🚨 关键配置项
# Nginx反向代理配置
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;

# 保持TCP长连接
proxy_set_header Connection "upgrade";

📊 监控指标建议

  1. 活跃连接数统计
  2. 心跳丢失率监控
  3. 异常断开报警
  4. 线程池使用情况

四、方案优化建议

生产环境增强

  • 添加JVM级别监控
  • 集成分布式连接管理
  • 实现灰度发布能力
  • 增加流量控制模块

💡 高级特性扩展

// 示例:心跳数据埋点
session.getBasicRemote().sendPing(ByteBuffer.wrap(
    ("心跳数据:" + System.currentTimeMillis()).getBytes()
));

五、常见问题解答

Q1:为什么需要双重心跳机制?
由于某些网络设备会过滤WebSocket控制帧,应用层心跳作为补充保障

Q2:如何选择合适的心跳间隔?
建议根据实际网络环境测试,通常移动端建议15-30秒,PC端30-60秒

Q3:服务端资源如何回收?
必须实现Session关闭的三重保障:

  1. 显式调用session.close()
  2. 取消定时任务
  3. 移除连接缓存

原创声明:本文采用 CC BY-NC-SA 4.0 协议授权,转载请注明出处!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值