稀土掘金技术社区

WebSocket 心得分享

一、前言

❝

本文将介绍 WebSocket 的封装,比如:心跳机制,重连和一些问题如何去处理

❞

二、背景

之前,钱包相关的查询,我们是使用的轮询方案来做的,后来更新了一次需求,需要做一些实时数据统计的更新,然后顺带给钱包的余额也用长连接来做了,好,那么故事就开始了...

某天,

「老板:」 我钱怎么没了,但是我这里查账户还有。

「我的内心:」 恩?这玩意难道说... 后端没返?

和后端沟通以后,感觉是返回了的,被挤账号了?排查了一段时间以后,最终我将问题锁定在手机息屏的操作上。

因为我们是一个 「H5」 的项目,APP 是嵌套在 webview 中,所以不能操作原生的事件来处理,只能将方案控制在浏览器提供的事件来处理。

好了,接下来各位可以看我是如何处理这个问题,如果没有搞过也是可以有不少收获,也欢迎大神评论区交流其他方案。

三、WebSocket

3.1 什么是 WebSocket ?为什么使用他?

以下是百度百科中对 「WebSocket」 的定义:

WebSocket 是一种在单个 TCP 连接上进行 全双工 通信的协议。WebSocket  通信协议于2011年被 IETF 定为标准 RFC 6455,并由 RFC7936 补充规范。WebSocket API 也被 W3C 定为标准。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

「WebSocket 的关键特点」

  1. 「双向通信(Full Duplex)」

  • 客户端和服务器都可以主动发送数据,而不是像 HTTP 一样只能由客户端发起请求。
  • 「实时性」

    • 消息可以实时传递,延迟更低,适合需要实时更新的场景。
  • 「持久化连接」

    • 使用单个 TCP 连接完成多次数据交互,无需为每次通信重新建立连接。
  • 「轻量级协议」

    • WebSocket 头部信息非常小,比传统 HTTP 请求的头部要轻量。
  • 「节约资源」

    • 长连接减少了资源消耗,特别是在频繁通信的场景中。

    上述中,是 AI 给我们总结的 WebSocket 的特点,接下来我们要知道我们为什么使用他,HTTP 他能不能做,他的局限性又在哪里?

    「传统 HTTP 的局限性:」

    1. HTTP 是基于请求-响应模型的,客户端必须发起请求,服务器才能返回数据。
    2. 如果需要实时更新(如股票价格、在线聊天),通常需要使用轮询(Polling)或长轮询(Long Polling),这会导致:
    • 高资源消耗(频繁的连接建立和断开)。
    • 高网络流量(每次请求都包含冗长的 HTTP 头部信息)。
    • 更高的延迟(数据可能需要等待较长时间才能返回)。

    其实 HTTP 是可以实现的,如果 HTTP 请求频繁三次握手和四次挥手的操作会占用大量资源,HTTP/1.1 以后开启了 「Keep-Alive (长连接)」,可以复用连接,但是实时的情况下,响应模型仍然会导致较高的延迟和资源消耗。

    相比之下,WebSocket 通过一次握手建立连接以后,就可以保持双向通信,服务器可以主动推送数据,无需客户端轮询。解决了 HTTP 带来的一些痛点。

    四、封装 WebSocket

    我们将实现以下几个功能点:

    • 「重连」
    • 「心跳机制」
    • 「事件回调」
    • 「连接状态管理」
    • 「销毁」

    4.1 Javascript 版本

    class ReSocket {
      constructor(url, options = {}) {
        this.url = url; // WebSocket 服务器地址
        this.options = options; // 可选参数
        this.socket = null; // WebSocket 实例
        this.maxReconnectTimes = options.maxReconnectTimes || 5; // 最大重连次数
        this.reconnectTimes = 0; // 当前重连次数
        this.reconnectInterval = options.reconnectInterval || 3000; // 重连间隔时间(毫秒)
        this.isClosed = false; // 是否已关闭
        this.isOpen = false; // 是否已打开
        this.isConnect = false; // 是否已连接
        this.isReconnecting = false; // 是否正在重连
        this.isDestroyed = false; // 是否已销毁
        this.reconnectTimer = null; // 重连定时器
        this.heartbeatTimer = null; // 心跳定时器
        this.heartbeatInterval = options.heartbeatInterval || 30000; // 心跳间隔时间(默认30秒)
        this.heartbeatData = options.heartbeatData || "ping"; // 心跳数据
        this.onMessageCallback = null; // 消息接收回调
        this.onOpenCallback = null; // 连接成功回调
        this.onCloseCallback = null; // 连接关闭回调
      }

        // 创建WebSocket实例
      createSocket() {
        this.socket = new WebSocket(this.url);

        this.socket.onopen = () => {
          this.isOpen = true;
          this.isConnect = true;
          this.reconnectTimes = 0; // 重连次数归零
          this.startHeartbeat(); // 启动心跳机制
          if (this.onOpenCallback) this.onOpenCallback(); // 调用连接成功回调
        };

        this.socket.onmessage = event => {
          if (this.onMessageCallback) this.onMessageCallback(event.data); // 调用消息接收回调
        };

        this.socket.onclose = () => {
          this.isOpen = false;
          this.isConnect = false;
          this.stopHeartbeat(); // 停止心跳机制
          if (this.onCloseCallback) this.onCloseCallback(); // 调用连接关闭回调
          if (!this.isClosed && this.reconnectTimes < this.maxReconnectTimes) {
            this.reconnect(); // 尝试重连
          }
        };

        this.socket.onerror = error => {
          console.error("WebSocket 错误: ", error); // 错误处理
        };
      }

      // 开始连接
      connect() {
        if (this.isDestroyed) return; // 如果已销毁,则不再连接
        this.createSocket(); // 创建WebSocket实例
      }

      // 重连
      reconnect() {
        if (this.isReconnecting || this.reconnectTimes >= this.maxReconnectTimes)
          return; // 防止重复重连

        this.isReconnecting = true;
        this.reconnectTimes++; // 增加重连次数

        this.reconnectTimer = setTimeout(() => {
          console.log(`正在重连... (${this.reconnectTimes})`); // 打印重连次数
          this.createSocket(); // 再次创建WebSocket实例
          this.isReconnecting = false; // 重连状态设置为false
        }, this.reconnectInterval); // 按设定时间重连
      }

      // 发送消息
      send(data) {
        if (this.isOpen) {
          this.socket.send(data); // 发送数据
        } else {
          console.error("WebSocket 未打开,无法发送消息。"); // 提示错误
        }
      }

      // 设置消息接收回调
      onMessage(callback) {
        this.onMessageCallback = callback; // 绑定接收消息的回调
      }

      // 设置连接成功回调
      onOpen(callback) {
        this.onOpenCallback = callback; // 绑定连接成功的回调
      }

      // 设置连接关闭回调
      onClose(callback) {
        this.onCloseCallback = callback; // 绑定连接关闭的回调
      }

      // 启动心跳机制
      startHeartbeat() {
        this.heartbeatTimer = setInterval(() => {
          if (this.isOpen) {
            this.send(this.heartbeatData); // 发送心跳数据
          }
        }, this.heartbeatInterval); // 按设定的时间间隔发送
      }

      // 停止心跳机制
      stopHeartbeat() {
        if (this.heartbeatTimer) {
          clearInterval(this.heartbeatTimer); // 清除心跳定时器
          this.heartbeatTimer = null;
        }
      }

      // 关闭连接
      close() {
        this.isClosed = true; // 设置为已关闭
        this.isOpen = false;
        this.socket.close(); // 关闭WebSocket连接
        this.stopHeartbeat(); // 停止心跳机制
        clearTimeout(this.reconnectTimer); // 清除重连定时器
      }

      // 销毁实例
      destroy() {
        this.isDestroyed = true; // 设置为已销毁
        this.close(); // 关闭连接
      }
    }

    4.2 Typescript 版本

    type ReSocketOptions = {
      maxReconnectTimes?: number; // 最大重连次数
      reconnectInterval?: number; // 重连间隔时间(毫秒)
      heartbeatInterval?: number; // 心跳间隔时间(毫秒)
      heartbeatData?: string; // 心跳数据
    };

    class ReSocket {
      private url: string;
      private socket: WebSocket | null = null;
      private maxReconnectTimes: number;
      private reconnectTimes: number = 0;
      private reconnectInterval: number;
      private isClosed: boolean = false;
      private isOpen: boolean = false;
      private isConnect: boolean = false;
      private isReconnecting: boolean = false;
      private isDestroyed: boolean = false;
      private reconnectTimer: NodeJS.Timeout | null = null;
      private heartbeatTimer: NodeJS.Timeout | null = null;
      private heartbeatInterval: number;
      private heartbeatData: string;
      private onMessageCallback: ((message: string) => void) | null = null;
      private onOpenCallback: (() => void) | null = null;
      private onCloseCallback: (() => void) | null = null;

      constructor(url: string, options: ReSocketOptions = {}) {
        this.url = url;
        this.maxReconnectTimes = options.maxReconnectTimes || 5;
        this.reconnectInterval = options.reconnectInterval || 3000;
        this.heartbeatInterval = options.heartbeatInterval || 30000;
        this.heartbeatData = options.heartbeatData || 'ping';
      }

      private createSocket(): void {
        this.socket = new WebSocket(this.url);

        this.socket.onopen = () =>

     {
          this.isOpen = true;
          this.isConnect = true;
          this.reconnectTimes = 0;
          this.startHeartbeat();
          if (this.onOpenCallback) this.onOpenCallback();
        };

        this.socket.onmessage = (event: MessageEvent) => {
          if (this.onMessageCallback) this.onMessageCallback(event.data);
        };

        this.socket.onclose = () => {
          this.isOpen = false;
          this.isConnect = false;
          this.stopHeartbeat();
          if (this.onCloseCallback) this.onCloseCallback();
          if (!this.isClosed && this.reconnectTimes < this.maxReconnectTimes) {
            this.reconnect();
          }
        };

        this.socket.onerror = (error: Event) => {
          console.error("WebSocket 错误: ", error);
        };
      }

      public connect(): void {
        if (this.isDestroyed) return;
        this.createSocket();
      }

      private reconnect(): void {
        if (this.isReconnecting || this.reconnectTimes >= this.maxReconnectTimes) return;

        this.isReconnecting = true;
        this.reconnectTimes++;

        this.reconnectTimer = setTimeout(() => {
          console.log(`正在重连... (${this.reconnectTimes})`);
          this.createSocket();
          this.isReconnecting = false;
        }, this.reconnectInterval);
      }

      public send(data: string): void {
        if (this.isOpen && this.socket) {
          this.socket.send(data);
        } else {
          console.error("WebSocket 未打开,无法发送消息。");
        }
      }

      public onMessage(callback: (message: string) => void): void {
        this.onMessageCallback = callback;
      }

      public onOpen(callback: () => void): void {
        this.onOpenCallback = callback;
      }

      public onClose(callback: () => void): void {
        this.onCloseCallback = callback;
      }

      private startHeartbeat(): void {
        this.heartbeatTimer = setInterval(() => {
          if (this.isOpen && this.socket) {
            this.send(this.heartbeatData);
          }
        }, this.heartbeatInterval);
      }

      private stopHeartbeat(): void {
        if (this.heartbeatTimer) {
          clearInterval(this.heartbeatTimer);
          this.heartbeatTimer = null;
        }
      }

      public close(): void {
        this.isClosed = true;
        this.isOpen = false;
        if (this.socket) {
          this.socket.close();
        }
        this.stopHeartbeat();
        if (this.reconnectTimer) {
          clearTimeout(this.reconnectTimer);
        }
      }

      public destroy(): void {
        this.isDestroyed = true;
        this.close();
      }
    }

    export { ReSocket };

    4.3 如何使用?

    首先简单写个 ws 的服务,我的 「Node」 环境是 20.18.0

    创建一个 Socket 的文件夹 vscode  打开执行:

    npm init -y

    生成完毕 package.json 之后,我们安装 ws :

    npm i ws

    创建 app.js 写一个简单服务 :

    const WebSocket = require("ws");

    // 创建 WebSocket 服务器,监听端口 8080
    const wss = new WebSocket.Server({ port: 8080 });

    // 监听客户端连接
    wss.on("connection", (ws) => {
      console.log("客户端已连接");

      // 监听客户端发送的消息
      ws.on("message", (message) => {
        console.log("收到客户端消息:", message);

        // 向客户端发送回复
        ws.send(`服务器回复: ${message}`);
      });

      // 发送一条欢迎消息给客户端
      ws.send("欢迎连接 WebSocket 服务器");
    });

    // 打印服务器地址
    console.log("WebSocket 服务器已启动: ws://localhost:8080");

    执行运行命令 :

    node .\app.js

    这里可以先用一个 WebSocket 调试工具试试是否创建成功 这里我是用的是 WebSocket在线测试工具 ,效果如下图:

    Image

    看到欢迎连接的时候,说明我们这个服务已经成功启动了,接下来就是 Javascript 中如何使用了,创建一个 index.html ,然后引入我们封装好的

    <!DOCTYPE html>
    <html lang="en">

    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>ws 调试</title>
    </head>
    <script src="./socket.js"></script>
    <script>
        var ws = new ReSocket('ws://localhost:8080');

        ws.connect()
        ws.onMessage((res) => {
            console.log('onMessage', res)
        })

    </script>

    <body>

    </body>

    </html>

    打开浏览器之后在控制台中看日志,如图:

    Image

    在网络中我们需要在这里看:

    Image

    到这里,如果你跟着做了一遍,你已经掌握了,如果感觉现在没时间,可以收藏,点赞,留个标记,毕竟收藏等于学会了😎😎😎

    五、我的痛点如何处理

    其实我的封装对于很多浏览器都是可以跑的,如果你复制去跑不了,那就人跑,明白我的意思吧?好了,其实这个封装,没有一些特殊兼容,比如:

    • 浏览器兼容性,某些浏览器支持不完整,可能就要降级处理了,具体某些说的是哪个浏览器,大家心里都知道
    • 代理和网络环境问题,某些企业网络或代理服务器会拦截或限制 WebSocket 流量
    • 网络状态检测,在网络断开但没有触发 onclose 或 onerror 时,可能无法及时重新连接
    • 心跳包超时检测,如果服务器没有正确响应心跳包,客户端可能无法及时发现连接异常
    • 大数据传输和分片处理,发送大数据包可能导致超时或失败
    • 浏览器生命周期事件,在浏览器后台或移动设备息屏时,WebSocket 可能被挂起或断开❗❗❗ 我的问题就是在这里

    等等...

    所以,需要各位根据自己使用场景,简单的需求基本上还是可以用的,如果场景涵盖比较多,这时候就可以优先考虑三方库使用

    「浏览器生命周期事件:」 当我在移动设备息屏时,我的 WebSocket 确实不会触发心跳,然后后端就给我挂了,那么我们浏览器其实提供了一个 visibilitychange 给我们使用,可以这样写:

    // 页面可见性监听
    document.addEventListener("visibilitychange", () => {
      if (document.visibilityState === "visible") {
        console.log("页面可见,尝试恢复 WebSocket 连接...");
        if (!socket.isConnect) {
          socket.connect(); // 页面可见时尝试恢复连接
        }
      } else {
        console.log("页面隐藏,关闭 WebSocket 连接...");
        socket.close(); // 页面隐藏时关闭连接以节省资源
      }
    });

    其实,这个我感觉还不太满足我,所以我又添加了一个定时任务来执行检验,代码如下:

    // 页面可见性监听
    document.addEventListener("visibilitychange", () => {
      if (document.visibilityState === "visible") {
        console.log("页面可见,尝试恢复 WebSocket 连接...");
        if (!socket.isConnect) {
          socket.connect();
        }
        lastActiveTime = Date.now(); // 更新最近活动时间
      } else {
        console.log("页面隐藏,关闭 WebSocket 连接...");
        socket.close();
      }
    });

    // 定时任务 - 检测 WebSocket 状态及页面活跃度
    const startCheckInterval = () => {
      checkInterval = setInterval(() => {
        const now = Date.now();

        // 检测 WebSocket 是否断开,尝试重连
        if (!socket.isConnect) {
          console.log("WebSocket 未连接,尝试重连...");
          socket.connect();
        }

        // 检测最近活动时间,判断页面是否处于非活跃状态
        if (now - lastActiveTime > 10000) { // 超过10秒未活动
          console.log("检测到页面可能处于非活跃状态!");
          // 此处可执行其他恢复或提醒操作
        }
      }, 5000); // 每5秒检查一次
    };

    // 清理定时任务
    const clearCheckInterval = () => {
      if (checkInterval) {
        clearInterval(checkInterval);
        checkInterval = null;
      }
    };

    // 初始化定时任务
    startCheckInterval();

    // 页面销毁的时候调用 clearCheckInterval 清理

    结语

    很久没有更新了,狠狠的写了3000多字,希望这篇文章还是对读者们有帮助。最近也是经历了裁员,和找工作一系列的事情,小小吐槽以下,就业环境不容乐观,但是基本上看见这篇文章的读者,都是热爱技术的,多学点知识基本上储备量上去了,面试还是很容易通过的。

    「最后,看到此刻的你,祝你工作顺利,生活愉快!」

    点击关注公众号,“技术干货” 及时达!