🔌 WebSocket如何判断在线离线

📅 发布于 2026年3月 | 👤 作者:博主 | 🏷️ 标签:WebSocket, 在线状态, 心跳机制, readyState, 实时通信, Web开发, 前端, 面试

欢迎来到我的博客文章!所有文章都是满满的前端干货,文章简明扼要。

核心结论:readyState 只能判断"连接状态",心跳机制才能判断"真实在线"。生产环境必须结合 连接状态 + 心跳检测 + 网络监听 三层判断

一、判断层级总览

┌─────────────────────────────────────────────────────────┐ │ Layer 3: 业务在线状态(心跳 + 最后活跃时间) │ ← 最可靠 │ "用户是否真的能收到消息?" │ ├─────────────────────────────────────────────────────────┤ │ Layer 2: 网络连接状态(navigator.onLine) │ ← 辅助参考 │ "设备是否有网络?" │ ├─────────────────────────────────────────────────────────┤ │ Layer 1: WebSocket 连接状态(readyState) │ ← 最基础 │ "TCP 连接是否打开?" │ └─────────────────────────────────────────────────────────┘

💡 关键认知:readyState === OPEN ≠ 用户在线(可能是"假连接")

只有心跳正常 + 连接打开 = 真实在线

二、完整在线状态判断矩阵

判断维度 在线条件 离线条件 可靠性
readyState === OPEN !== OPEN ⭐⭐
navigator.onLine === true === false ⭐⭐
心跳响应 收到 pong 超时未收到 ⭐⭐⭐⭐⭐
最后活跃时间 < 60 秒 > 60 秒 ⭐⭐⭐⭐
服务端记录 Redis 存在 Redis 过期 ⭐⭐⭐⭐⭐

三个层面判断:

  1. 连接层:检查 readyState 是否为 OPEN,这是基础但不够可靠。
  2. 网络层:监听 navigator.onLine 判断设备是否有网络。
  3. 业务层:核心是心跳机制,客户端 30 秒发 ping,服务端回复 pong,超时未收到判定离线。

服务端侧,我会用 Redis 存储在线状态,key 设置 60 秒过期,心跳时续期,过期自动判定离线。

之前项目中,我还加了状态防抖,避免网络波动导致 UI 频繁闪烁,用户体验提升明显。

Layer 1: WebSocket 连接状态(readyState)

1.1 readyState 的四种状态

常量 含义 说明
CONNECTING 0 连接中 正在建立连接,尚未完成握手
OPEN 1 已连接 连接已建立,可以发送数据
CLOSING 2 关闭中 正在关闭连接
CLOSED 3 已关闭 连接已关闭或无法打开

1.2 基础判断代码

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.3 readyState 的局限性 ⚠️

问题 1:假连接(Zombie Connection)

问题 2:延迟感知

问题 3:无法区分"连接"与"在线"

Layer 2: 网络连接状态(navigator.onLine)

2.1 基础用法

// 检查当前网络状态
console.log(navigator.onLine); // true 或 false

// 监听网络变化
window.addEventListener('online', () => {
  console.log('网络已连接');
  // 尝试重连 WebSocket
  reconnectWebSocket();
});

window.addEventListener('offline', () => {
  console.log('网络已断开');
  // 更新 UI 状态
  updateUIStatus('offline');
});

2.2 局限性

Layer 3: 心跳机制(最可靠 ✅)

3.1 心跳机制原理

客户端 服务端 │ │ ├──────── ping ────────────────>│ (每 30 秒) │ │ │<──────── pong ────────────────┤ (立即响应) │ │ │ (收到 pong,重置超时计时器) │ │ │ ├──────── ping ────────────────>│ (30 秒后) │ │ │ (超时 10 秒未收到) │ │ │ └──────── 判定离线 ──────────────┘

3.2 完整实现代码

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);

3.3 服务端实现(Node.js + Redis)

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:', ''));
}

生产环境最佳实践

1. 状态防抖(避免 UI 闪烁)

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;
  }
}

2. 综合判断逻辑

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: '正常在线' };
}

3. 心跳参数建议

场景 心跳间隔 超时时间 说明
即时通讯(IM) 30 秒 10 秒 要求快速感知离线
在线协作 30 秒 15 秒 平衡实时性和性能
数据推送 60 秒 20 秒 降低服务器压力
物联网设备 120 秒 30 秒 节省流量和电量

面试回答模板

"WebSocket 判断在线离线需要三个层面:

  1. 连接层:检查 readyState 是否为 OPEN,这是基础但不够可靠。
  2. 网络层:监听 navigator.onLine 判断设备是否有网络。
  3. 业务层:核心是心跳机制,客户端 30 秒发 ping,服务端回复 pong,超时未收到判定离线。

服务端侧,我会用 Redis 存储在线状态,key 设置 60 秒过期,心跳时续期,过期自动判定离线。

之前项目中,我还加了状态防抖,避免网络波动导致 UI 频繁闪烁,用户体验提升明显。"

常见问题

Q1: 为什么不能只用 readyState 判断?

A: readyState 只能判断 TCP 连接状态,无法感知"假连接"(如网络突然断开、设备休眠)。只有心跳机制才能确认双向通信正常。

Q2: 心跳间隔设置多少合适?

A: 取决于业务场景。IM 类应用建议 30 秒,数据推送类可以 60 秒。间隔太短增加服务器压力,太长无法及时感知离线。

Q3: 服务端如何高效存储在线状态?

A: 使用 Redis 的 SETEX 命令,key 为 online:userId,value 为时间戳,TTL 设置为心跳间隔的 2 倍。心跳时自动续期,过期自动删除。

Q4: 如何避免网络波动导致频繁离线/在线切换?

A: 使用状态防抖,离线状态延迟 3 秒更新 UI,在线状态立即更新。避免短暂网络波动导致 UI 闪烁。

核心总结

← 返回首页