WebSocket双端心跳保活机制完整实现方案
一、为什么需要心跳机制?
- 防止中间设备(NAT/防火墙)断开「空闲连接」
- 及时检测断线情况(网络波动、客户端崩溃)
- 保持双端连接状态同步
- 避免僵尸连接占用服务资源
二、完整技术方案
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();
}
}
}
}
关键代码说明:
-
线程池配置
private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors());
- 使用独立线程池处理心跳任务
- 根据CPU核心数动态分配线程
-
双心跳触发逻辑
// 协议层Ping session.getBasicRemote().sendPing(null); // 应用层心跳检测 if ("HEARTBEAT".equals(message)) { sendMessage("HEARTBEAT_ACK"); }
-
连接关闭处理
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";
📊 监控指标建议:
- 活跃连接数统计
- 心跳丢失率监控
- 异常断开报警
- 线程池使用情况
四、方案优化建议
✅ 生产环境增强:
- 添加JVM级别监控
- 集成分布式连接管理
- 实现灰度发布能力
- 增加流量控制模块
💡 高级特性扩展:
// 示例:心跳数据埋点
session.getBasicRemote().sendPing(ByteBuffer.wrap(
("心跳数据:" + System.currentTimeMillis()).getBytes()
));
五、常见问题解答
❓ Q1:为什么需要双重心跳机制?
由于某些网络设备会过滤WebSocket控制帧,应用层心跳作为补充保障
❓ Q2:如何选择合适的心跳间隔?
建议根据实际网络环境测试,通常移动端建议15-30秒,PC端30-60秒
❓ Q3:服务端资源如何回收?
必须实现Session关闭的三重保障:
- 显式调用session.close()
- 取消定时任务
- 移除连接缓存
原创声明:本文采用 CC BY-NC-SA 4.0 协议授权,转载请注明出处!