一个项目教你正确使用WebSocket

72 阅读7分钟

websocket的官方解释

一、WebSocket 基础回顾

WebSocket 是在单个 TCP 连接上提供全双工(双向实时)通信的协议。

简单说:服务器可以随时主动推消息给浏览器,不用你一直去轮询。

二、websocket与http的区别?

  • 通信模式:
    • HTTP是单项的(客户端请求,服务器响应)。
    • Websocket是双向的(客户端和服务器可以随时发送消息)。
  • 连接方式:
    • HTTP是无状态的,每次请求都需要重新建立连接。
    • Websocket是持久连接,建立后可以持续通信。
  • 性能:
    • WebSocket减少了每次通信的开销,适合实时性要求高的场景。

三、WebSocket 的四大生命周期事件

WebSocket可能用到的生命周期

onopen → 连接成功建立时

onmessage → 每收到一条服务器消息时

onerror → 连接发生错误时(注意:错误后很快会触发 onclose)

onclose → 连接关闭时(正常关闭或异常断开都会触发)

简单的websocket连接 
const websocket = new WebSocket('ws://192.168.1.1:8000/example/websocket');

websocket.onopen = function () { console.log('连接成功'); };

websocket.onmessage = function (event) { console.log(event.data, 'event.data打印'); }; 

websocket.onerror = function (event) { console.log(event, 'event打印'); }; 

websocket.onclose = function (event) { console.log('连接关闭', event.code, event.reason); };

四、原生Websocket入门。

WebSocket 的用法简单到离谱,真正常用的就这四件事:

new WebSocket(url)  → 建立连接
ws.send(data)  → 发消息
ws.onmessage  → 收消息(项目里 90% 的逻辑都写在这里)
ws.close()  → 关连接(一定要记得关,不然会漏连接!)
<body>
  <h1>websocket练习</h1>
  <div style="padding: 20px;">
    <div>
      <input type="text" id="messageInput" placeholder="输入要发送的消息" style="padding: 8px; width: 300px;">
      <button onclick="sendMessage()">发送消息</button>
    </div>
    <div style="margin-top: 10px;">
      <button onclick="closeConnection()">关闭连接</button>
    </div>
    <div id="status" style="margin-top: 20px; padding: 10px; background: #f0f0f0;">
      状态:连接中...
    </div>
  </div>

  <script>
    // 1. new WebSocket(url) → 建立连接
    const websocket = new WebSocket('ws://192.168.1.1:8000/data/status');

    websocket.onopen = function () {
      console.log('连接成功');
      document.getElementById('status').textContent = '状态:已连接';
      document.getElementById('status').style.background = '#d4edda';
    };

    // 3. ws.onmessage → 收消息(项目里 90% 的逻辑都写在这里)
    websocket.onmessage = function (event) {
      console.log('收到消息:', event.data);
      const statusDiv = document.getElementById('status');
      statusDiv.innerHTML = `状态:已连接<br>收到消息: ${event.data}`;
    };

    websocket.onerror = function (event) {
      console.log('连接错误:', event);
      document.getElementById('status').textContent = '状态:连接错误';
      document.getElementById('status').style.background = '#f8d7da';
    };

    websocket.onclose = function (event) {
      console.log('连接关闭', event.code, event.reason);
      document.getElementById('status').textContent = `状态:连接已关闭 (代码: ${event.code})`;
      document.getElementById('status').style.background = '#fff3cd';
    };

    // 2. ws.send(data) → 发消息
    function sendMessage() {
      const input = document.getElementById('messageInput');
      const message = input.value.trim();

      if (websocket.readyState === WebSocket.OPEN) {
        websocket.send(message);
        console.log('发送消息:', message);
        input.value = '';
      } else {
        alert('WebSocket 未连接,无法发送消息');
        console.log('WebSocket 状态:', websocket.readyState);
      }
    }

    // 4. ws.close() → 关连接(一定要记得关,不然会漏连接!)
    function closeConnection() {
      if (websocket.readyState === WebSocket.OPEN || websocket.readyState === WebSocket.CONNECTING) {
        websocket.close();
        console.log('主动关闭连接');
      } else {
        alert('连接已关闭或未连接');
      }
    }

    // 页面关闭时自动关闭连接(防止内存泄漏)
    window.addEventListener('beforeunload', function () {
      if (websocket.readyState === WebSocket.OPEN) {
        websocket.close();
      }
    });
  </script>

</body>

五、websocket中心跳机制与自动重连

AI举个生活例子:

  • 心跳:就像你和女朋友每隔半小时发一句“我在呢”,这样她就不会以为你挂了。
  • 自动重连:你们电话真的断了,你再重新拨一次。

心跳机制需要变量:

1、需要一个变量记录心跳间隔时间heartbeatInterval(数值型)。

2、需要有一个超时检测变量pongReceived(布尔值,true/false)。

发 ping 用 setInterval 最稳定

重连机制需要变量:

1、设置最大重连次数变量。

2、重连次数的计数器变量。

3、设置一个延迟时间设置上限。

<body>
  <h1>websocket练习</h1>
  <div style="padding: 20px;">
    <div>
      <input type="text" id="messageInput" placeholder="输入要发送的消息" style="padding: 8px; width: 300px;">
      <button onclick="sendMessage()">发送消息</button>
    </div>
    <div style="margin-top: 10px;">
      <button onclick="closeConnection()">关闭连接</button>
    </div>
    <div id="status" style="margin-top: 20px; padding: 10px; background: #f0f0f0;">
      状态:连接中...
    </div>
  </div>

  <script>
    // 1. new WebSocket(url) → 建立连接
    const websocket = new WebSocket('ws://192.168.1.1:8000/data/status');

    // 心跳机制相关变量
    let heartbeatTimer = null;        // 心跳定时器
    let reconnectTimer = null;        // 重连定时器
    let heartbeatInterval = 30000;    // 心跳间隔:30秒(可根据服务器要求调整)
    let reconnectDelay = 3000;        // 重连延迟:3秒
    let isManualClose = false;        // 是否手动关闭

    websocket.onopen = function () {
      console.log('连接成功');
      document.getElementById('status').textContent = '状态:已连接';
      document.getElementById('status').style.background = '#d4edda';

      // 启动心跳机制
      startHeartbeat();
    };

    // 3. ws.onmessage → 收消息(项目里 90% 的逻辑都写在这里)
    websocket.onmessage = function (event) {
      console.log('收到消息:', event.data);
      const statusDiv = document.getElementById('status');
      statusDiv.innerHTML = `状态:已连接<br>收到消息: ${event.data}`;
    };

    websocket.onerror = function (event) {
      console.log('连接错误:', event);
      document.getElementById('status').textContent = '状态:连接错误';
      document.getElementById('status').style.background = '#f8d7da';
    };

    websocket.onclose = function (event) {
      console.log('连接关闭', event.code, event.reason);
      document.getElementById('status').textContent = `状态:连接已关闭 (代码: ${event.code})`;
      document.getElementById('status').style.background = '#fff3cd';

      // 停止心跳
      stopHeartbeat();

      // 如果不是手动关闭,则尝试重连
      if (!isManualClose) {
        console.log('尝试重新连接...');
        reconnectTimer = setTimeout(function () {
          location.reload(); // 或者可以重新创建 WebSocket 连接
        }, reconnectDelay);
      }
    };

    // 2. ws.send(data) → 发消息
    function sendMessage() {
      const input = document.getElementById('messageInput');
      const message = input.value.trim();

      if (websocket.readyState === WebSocket.OPEN) {
        websocket.send(message);
        console.log('发送消息:', message);
        input.value = '';
      } else {
        alert('WebSocket 未连接,无法发送消息');
        console.log('WebSocket 状态:', websocket.readyState);
      }
    }

    // 心跳机制:定期发送心跳消息保持连接
    function startHeartbeat() {
      stopHeartbeat(); // 先清除可能存在的定时器

      heartbeatTimer = setInterval(function () {
        if (websocket.readyState === WebSocket.OPEN) {
          // 发送心跳消息(根据服务器要求,可能是 'ping'、'heartbeat' 或 JSON 格式)
          websocket.send(JSON.stringify({ type: 'ping' }));
          console.log('发送心跳消息');
        } else {
          // 如果连接已关闭,停止心跳
          stopHeartbeat();
        }
      }, heartbeatInterval);
    }

    function stopHeartbeat() {
      if (heartbeatTimer) {
        clearInterval(heartbeatTimer);
        heartbeatTimer = null;
      }
    }

    // 4. ws.close() → 关连接(一定要记得关,不然会漏连接!)
    function closeConnection() {
      isManualClose = true; // 标记为手动关闭
      stopHeartbeat(); // 停止心跳

      if (websocket.readyState === WebSocket.OPEN || websocket.readyState === WebSocket.CONNECTING) {
        websocket.close();
        console.log('主动关闭连接');
      } else {
        alert('连接已关闭或未连接');
      }
    }

    // 页面关闭时自动关闭连接(防止内存泄漏)
    window.addEventListener('beforeunload', function () {
      isManualClose = true; // 标记为手动关闭,避免触发重连
      stopHeartbeat(); // 停止心跳
      if (websocket.readyState === WebSocket.OPEN) {
        websocket.close();
      }
    });
  </script>

</body>

六、封装Websocket方法。

  第一步:连接管理 + 自动重连 + 状态监听(附代码)

  第二步:消息总线 + 消息去重 + 消息队列(断网缓存)

  第三步:心跳检测 + 超时主动断开

/**
 * websocket 管理器
 * @param {string} url - websocket 地址
 * @param {Object} options - 配置选项
 * @param {number} options.maxReconnectCount - 最大重连次数,默认 10
 * @param {number} options.heartbeatInterval - 心跳间隔时间(ms),默认 30000
 * @param {number} options.reconnectInterval - 重连间隔时间(ms),默认 3000
 * @param {boolean} options.autoConnect - 是否自动连接,默认 true
 * @param {boolean} options.enableHeartbeat - 是否启用心跳,默认 true
 * @param {Function} options.onOpen - 连接打开回调
 * @param {Function} options.onMessage - 消息接收回调
 * @param {Function} options.onError - 错误回调
 * @param {Function} options.onClose - 连接关闭回调
 */
class WebSocketManager {
  constructor(url, options = {}) {
    this.url = url;
    this.ws = null; // 原生 WebSocket 实例
    this.isConnected = false; // 当前是否真正连接成功
    this.isClosed = false; // 是否被手动关闭(手动关闭后不再自动重连)
    this.reconnectTimer = null; // 重连定时器
    this.heartbeatTimer = null; // 心跳定时器
    this.reconnectCount = 0; // 已重连次数

    // 配置
    this.maxReconnectCount = options.maxReconnectCount ?? 10;
    this.heartbeatInterval = options.heartbeatInterval || 30000;
    this.reconnectInterval = options.reconnectInterval || 3000;
    this.autoConnect = options.autoConnect !== false;
    this.enableHeartbeat = options.enableHeartbeat !== false;

    // 外部回调
    this.onOpenCallback = options.onOpen || null;
    this.onMessageCallback = options.onMessage || null;
    this.onErrorCallback = options.onError || null;
    this.onCloseCallback = options.onClose || null;

    if (this.autoConnect) this.init();
  }
  /**
   * 初始化 WebSocket 连接(核心入口)
   * 每次重连都会调用此方法,负责创建新的 WebSocket 实例并绑定所有事件
   */
  init() {
    if (this.isClosed) return;

    this.ws = new WebSocket(this.url);
    // 连接成功:重置重连次数,启动心跳,通知上层
    this.ws.onopen = () => {
      this.isConnected = true;
      this.reconnectCount = 0; // 成功连接必须清零!!!

      if (this.enableHeartbeat) {
        this.startHeartbeat();
      }
      this.onOpenCallback?.();
    };
    // 收到消息:统一处理心跳响应和业务消息
    this.ws.onmessage = (event) => {
      this.handleMessage(event.data);
    };
    // 连接出错:仅打印日志,不直接触发重连(onclose 会处理)
    this.ws.onerror = (err) => {
      console.error("WebSocket 错误:", err);
      this.isConnected = false;
      this.stopHeartbeat();

      this.onErrorCallback?.(err);
    };
    // 连接关闭:清理状态 + 判断是否需要自动重连
    this.ws.onclose = () => {
      this.isConnected = false;
      this.stopHeartbeat();
      this.ws = null;

      this.onCloseCallback?.();
      // 只有非手动关闭才触发重连
      if (!this.isClosed) {
        this.reconnect();
      }
    };
  }
  /**
   * 发送消息(安全发送)
   * 自动判断连接状态,未连接时直接丢弃(或后续可加入队列缓存)
   */
  send(data) {
    if (
      !this.isConnected ||
      !this.ws ||
      this.ws.readyState !== WebSocket.OPEN
    ) {
      console.warn("WebSocket 未连接或已关闭");
      return;
    }
    this.ws.send(typeof data === "object" ? JSON.stringify(data) : data);
  }
  /**
   * 统一消息处理中心
   * 负责:心跳响应识别 + 业务消息转发(支持 uni-app 和 纯 H5 两套事件总线)
   */
  handleMessage(data) {
    // 如果外部传了回调,优先走外部逻辑
    if (this.onMessageCallback) {
      this.onMessageCallback(data);
      return;
    }

    try {
      const message = JSON.parse(data);
      // 心跳响应直接消费,不向上抛
      if (message.type === "pong") return;

      // 业务消息:兼容 uni-app 和 纯浏览器
      if (typeof uni !== "undefined" && uni.$emit) {
        uni.$emit("ws-message", message);
      } else {
        window.dispatchEvent(
          new CustomEvent("ws-message", { detail: message })
        );
      }
    } catch (e) {
      // 非 JSON 消息原样转发(某些服务端发纯文本)
      if (typeof uni !== "undefined" && uni.$emit) {
        uni.$emit("ws-message", data);
      } else {
        window.dispatchEvent(new CustomEvent("ws-message", { detail: data }));
      }
    }
  }

  /**
   * 启动心跳机制
   * 负责:周期性发送心跳消息,并处理心跳响应
   */
  startHeartbeat() {
    this.stopHeartbeat();
    this.heartbeatTimer = setInterval(() => {
      this.send({ type: "ping" });
    }, this.heartbeatInterval);
  }
  /**
   * 停止心跳机制
   * 负责:清除心跳定时器
   */
  stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
  }
  /**
   * 重连机制
   * 负责:定时器控制重连次数和间隔
   */
  reconnect() {
    if (this.isClosed) return;
    if (this.reconnectTimer) return;

    this.reconnectTimer = setTimeout(() => {
      this.reconnectCount++;
      console.log(`开始第 ${this.reconnectCount} 次重连,目标: ${this.url}`);
      this.reconnectTimer = null;
      this.init();
    }, this.reconnectInterval);
  }
  /**
   * 关闭连接
   * 负责:标记关闭状态,清理定时器和连接实例
   */
  close() {
    this.isClosed = true;
    this.isConnected = false;
    this.stopHeartbeat();

    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }

    if (this.ws) {
      this.ws.close(); // 4. 直接 close()
      this.ws = null;
    }
  }

  /**
   * 手动重连
   * 负责:重置状态,重新初始化连接
   */
  reconnectManually() {
    this.isClosed = false;
    this.reconnectCount = 0;
    this.init();
  }

  /**
   * 获取连接状态
   * 负责:返回当前连接状态
   */
  getStatus() {
    return {
      isConnected: this.isConnected,
      isClosed: this.isClosed,
      reconnectCount: this.reconnectCount,
      url: this.url,
    };
  }
}

export default WebSocketManager;

七、使用方式

// store中使用
// stores/useWsStore.js
import { defineStore } from 'pinia'
import WebSocketManager from '@/utils/WebSocketManager.js'

export const useWsStore = defineStore('ws', {
  state: () => ({
    ws: null,              // WebSocketManager 实例
    connected: false,      // 连接状态
    messages: [],          // 所有历史消息(可选)
    latest: null,          // 最新一条数据
    isLoading: false,
    error: null,
  }),

  actions: {
    // 连接(整个项目只调用一次)
    connect(url = 'wss://your-server.com/ws') {
      if (this.ws) this.disconnect()

      this.isLoading = true
      this.ws = new WebSocketManager(url, {
        heartbeatInterval: 10000,
        reconnectInterval: 2000,

        onOpen: () => {
          this.connected = true
          this.isLoading = false
          this.error = null
          console.log('WebSocket 已连接')
        },

        onMessage: (data) => {
          // 统一把所有消息存起来
          this.latest = data
          this.messages.push({
            data,
            timestamp: Date.now(),
          })

          try {
            this.latest = JSON.parse(data)
          } catch {}
        },

        onError: (e) => {
          this.error = '连接出错'
          this.isLoading = false
        },

        onClose: () => {
          this.connected = false
          this.isLoading = false
          console.log('已断开,自动重连中...')
        },
      })
    },

    // 发送消息
    send(payload) {
      if (this.ws && this.connected) {
        this.ws.send(payload)
      } else {
        console.warn('WebSocket 未连接,消息已丢弃', payload)
      }
    },

    // 断开
    disconnect() {
      this.ws?.close()
      this.ws = null
      this.connected = false
      this.isLoading = false
    },

    // 清空消息记录
    clear() {
      this.messages = []
      this.latest = null
    },
  },
})
页面中使用

import { useWsStore } from '@/stores/useWsStore'
import { storeToRefs } from 'pinia'
import { onMounted } from 'vue'

const ws = useWsStore()
const { connected, latest, messages, isLoading } = storeToRefs(ws)

onMounted(() => {  ws.connect('wss://your-real-url.com/api/ws')  // 改成你的地址})

const sendHello = () => {  ws.send({ type: 'say', content: 'hello world' })}