欢迎来到我的博客文章!所有文章都是满满的前端干货,文章简明扼要。
核心结论:readyState 只能判断"连接状态",心跳机制才能判断"真实在线"。生产环境必须结合 连接状态 + 心跳检测 + 网络监听 三层判断
💡 关键认知:readyState === OPEN ≠ 用户在线(可能是"假连接")
只有心跳正常 + 连接打开 = 真实在线
| 判断维度 | 在线条件 | 离线条件 | 可靠性 |
|---|---|---|---|
| readyState | === OPEN | !== OPEN | ⭐⭐ |
| navigator.onLine | === true | === false | ⭐⭐ |
| 心跳响应 | 收到 pong | 超时未收到 | ⭐⭐⭐⭐⭐ |
| 最后活跃时间 | < 60 秒 | > 60 秒 | ⭐⭐⭐⭐ |
| 服务端记录 | Redis 存在 | Redis 过期 | ⭐⭐⭐⭐⭐ |
服务端侧,我会用 Redis 存储在线状态,key 设置 60 秒过期,心跳时续期,过期自动判定离线。
之前项目中,我还加了状态防抖,避免网络波动导致 UI 频繁闪烁,用户体验提升明显。
| 常量 | 值 | 含义 | 说明 |
|---|---|---|---|
| CONNECTING | 0 | 连接中 | 正在建立连接,尚未完成握手 |
| OPEN | 1 | 已连接 | 连接已建立,可以发送数据 |
| CLOSING | 2 | 关闭中 | 正在关闭连接 |
| CLOSED | 3 | 已关闭 | 连接已关闭或无法打开 |
const ws = new WebSocket('ws://example.com');
// 方法 1:直接判断数值
if (ws.readyState === 1) {
console.log('连接已打开');
}
// 方法 2:使用常量(推荐)
if (ws.readyState === WebSocket.OPEN) {
console.log('连接已打开');
}
// 完整状态判断
function getConnectionStatus(ws) {
switch (ws.readyState) {
case WebSocket.CONNECTING:
return '连接中...';
case WebSocket.OPEN:
return '已连接';
case WebSocket.CLOSING:
return '关闭中...';
case WebSocket.CLOSED:
return '已关闭';
default:
return '未知状态';
}
} 问题 1:假连接(Zombie Connection)
问题 2:延迟感知
问题 3:无法区分"连接"与"在线"
// 检查当前网络状态
console.log(navigator.onLine); // true 或 false
// 监听网络变化
window.addEventListener('online', () => {
console.log('网络已连接');
// 尝试重连 WebSocket
reconnectWebSocket();
});
window.addEventListener('offline', () => {
console.log('网络已断开');
// 更新 UI 状态
updateUIStatus('offline');
}); class WebSocketClient {
constructor(url) {
this.url = url;
this.ws = null;
this.heartbeatInterval = 30000; // 30 秒发一次心跳
this.heartbeatTimeout = 10000; // 10 秒未收到 pong 判定离线
this.heartbeatTimer = null;
this.heartbeatTimeoutTimer = null;
this.isOnline = false;
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket 已连接');
this.isOnline = true;
this.startHeartbeat(); // 开始心跳
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'pong') {
// 收到心跳响应,重置超时计时器
clearTimeout(this.heartbeatTimeoutTimer);
this.isOnline = true;
console.log('收到心跳响应,在线');
} else {
// 处理业务消息
this.handleMessage(data);
}
};
this.ws.onclose = () => {
console.log('WebSocket 已关闭');
this.isOnline = false;
this.stopHeartbeat();
// 尝试重连
setTimeout(() => this.connect(), 5000);
};
this.ws.onerror = (error) => {
console.error('WebSocket 错误', error);
this.isOnline = false;
};
}
startHeartbeat() {
this.stopHeartbeat(); // 清除旧的定时器
this.heartbeatTimer = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
// 发送心跳
this.ws.send(JSON.stringify({ type: 'ping' }));
// 启动超时计时器
this.heartbeatTimeoutTimer = setTimeout(() => {
console.warn('心跳超时,判定离线');
this.isOnline = false;
this.ws.close(); // 关闭连接,触发重连
}, this.heartbeatTimeout);
}
}, this.heartbeatInterval);
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
if (this.heartbeatTimeoutTimer) {
clearTimeout(this.heartbeatTimeoutTimer);
this.heartbeatTimeoutTimer = null;
}
}
// 获取真实在线状态
getOnlineStatus() {
return this.ws.readyState === WebSocket.OPEN && this.isOnline;
}
handleMessage(data) {
// 处理业务消息
console.log('收到消息', data);
}
}
// 使用
const client = new WebSocketClient('ws://example.com');
client.connect();
// 检查在线状态
setInterval(() => {
console.log('当前在线状态:', client.getOnlineStatus());
}, 5000); const WebSocket = require('ws');
const Redis = require('ioredis');
const redis = new Redis();
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws, req) => {
const userId = getUserIdFromRequest(req); // 从请求中获取用户 ID
console.log(`用户 ${userId} 已连接`);
// 存储在线状态到 Redis,60 秒过期
redis.setex(`online:${userId}`, 60, Date.now());
ws.on('message', async (message) => {
const data = JSON.parse(message);
if (data.type === 'ping') {
// 收到心跳,回复 pong
ws.send(JSON.stringify({ type: 'pong' }));
// 续期 Redis 中的在线状态
await redis.setex(`online:${userId}`, 60, Date.now());
console.log(`用户 ${userId} 心跳续期`);
} else {
// 处理业务消息
handleBusinessMessage(data, userId);
}
});
ws.on('close', async () => {
console.log(`用户 ${userId} 已断开`);
// 删除 Redis 中的在线状态
await redis.del(`online:${userId}`);
});
});
// 检查用户是否在线
async function isUserOnline(userId) {
const exists = await redis.exists(`online:${userId}`);
return exists === 1;
}
// 获取所有在线用户
async function getOnlineUsers() {
const keys = await redis.keys('online:*');
return keys.map(key => key.replace('online:', ''));
} class OnlineStatusManager {
constructor() {
this.status = 'online';
this.debounceTimer = null;
this.debounceDelay = 3000; // 3 秒防抖
}
updateStatus(newStatus) {
if (newStatus === this.status) return;
// 如果是离线状态,延迟更新(防抖)
if (newStatus === 'offline') {
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => {
this.status = newStatus;
this.notifyUI(newStatus);
}, this.debounceDelay);
} else {
// 如果是在线状态,立即更新
clearTimeout(this.debounceTimer);
this.status = newStatus;
this.notifyUI(newStatus);
}
}
notifyUI(status) {
// 更新 UI
document.getElementById('status').textContent = status;
document.getElementById('status').className = status;
}
} function getComprehensiveOnlineStatus(wsClient) {
// Layer 1: WebSocket 连接状态
const wsConnected = wsClient.ws.readyState === WebSocket.OPEN;
// Layer 2: 网络状态
const networkOnline = navigator.onLine;
// Layer 3: 心跳状态
const heartbeatOk = wsClient.isOnline;
// 综合判断
if (!networkOnline) {
return { status: 'offline', reason: '网络断开' };
}
if (!wsConnected) {
return { status: 'offline', reason: 'WebSocket 未连接' };
}
if (!heartbeatOk) {
return { status: 'offline', reason: '心跳超时' };
}
return { status: 'online', reason: '正常在线' };
} | 场景 | 心跳间隔 | 超时时间 | 说明 |
|---|---|---|---|
| 即时通讯(IM) | 30 秒 | 10 秒 | 要求快速感知离线 |
| 在线协作 | 30 秒 | 15 秒 | 平衡实时性和性能 |
| 数据推送 | 60 秒 | 20 秒 | 降低服务器压力 |
| 物联网设备 | 120 秒 | 30 秒 | 节省流量和电量 |
"WebSocket 判断在线离线需要三个层面:
服务端侧,我会用 Redis 存储在线状态,key 设置 60 秒过期,心跳时续期,过期自动判定离线。
之前项目中,我还加了状态防抖,避免网络波动导致 UI 频繁闪烁,用户体验提升明显。"
A: readyState 只能判断 TCP 连接状态,无法感知"假连接"(如网络突然断开、设备休眠)。只有心跳机制才能确认双向通信正常。
A: 取决于业务场景。IM 类应用建议 30 秒,数据推送类可以 60 秒。间隔太短增加服务器压力,太长无法及时感知离线。
A: 使用 Redis 的 SETEX 命令,key 为 online:userId,value 为时间戳,TTL 设置为心跳间隔的 2 倍。心跳时自动续期,过期自动删除。
A: 使用状态防抖,离线状态延迟 3 秒更新 UI,在线状态立即更新。避免短暂网络波动导致 UI 闪烁。