稀土掘金技术社区 2024年12月29日
WebSocket 心得分享
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入探讨了WebSocket的封装,重点介绍了心跳机制和重连策略的实现。文章首先解释了WebSocket相较于传统HTTP的优势,尤其在实时数据更新方面的优越性。接着,详细展示了如何使用JavaScript和TypeScript封装WebSocket,包括连接、消息发送、接收以及错误处理。核心功能包括自动重连、心跳保活、事件回调、连接状态管理和资源销毁。通过代码示例,读者可以了解如何构建一个健壮的WebSocket客户端,并避免因网络不稳定或设备休眠导致连接中断的问题。文章最后还提到了实际应用中可能遇到的兼容性、网络环境等问题,为开发者提供了参考。

📡 WebSocket 是一种在单个TCP连接上进行全双工通信的协议,它允许客户端和服务器之间进行实时双向数据传输,相较于传统的HTTP轮询,WebSocket 具有更低的延迟和更高的效率。

❤️ 为了维持WebSocket连接的稳定性,文章提出了心跳机制,即客户端定期向服务器发送心跳数据,以检测连接是否仍然有效,避免因网络中断或设备休眠导致连接失效。

🔄 文章详细阐述了WebSocket的重连机制,当连接意外断开时,客户端会自动尝试重新连接,并设置最大重连次数和重连间隔,确保在网络波动的情况下,应用依然可以保持与服务器的连接。

⚙️ 文章提供了JavaScript和TypeScript两种版本的WebSocket封装代码,展示了如何通过类来实现连接、发送、接收、心跳、重连等核心功能,并提供了详细的注释说明,方便开发者理解和使用。

🛠️ 文章还提到了实际应用中可能遇到的问题,如浏览器兼容性、网络环境限制等,并建议开发者考虑降级处理方案,以应对各种复杂情况。

原创 sakana 2024-12-29 09:01 重庆

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

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

一、前言

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

二、背景

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

某天,

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

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

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

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

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

三、WebSocket

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

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

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

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

「WebSocket 的关键特点」

    「双向通信(Full Duplex)」

「实时性」

「持久化连接」

「轻量级协议」

「节约资源」

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

「传统 HTTP 的局限性:」

    HTTP 是基于请求-响应模型的,客户端必须发起请求,服务器才能返回数据。

    如果需要实时更新(如股票价格、在线聊天),通常需要使用轮询(Polling)或长轮询(Long Polling),这会导致:

其实 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({ port8080 });
// 监听客户端连接
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在线测试工具 ,效果如下图:

看到欢迎连接的时候,说明我们这个服务已经成功启动了,接下来就是 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>

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

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

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

五、我的痛点如何处理

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

等等...

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

「浏览器生命周期事件:」 当我在移动设备息屏时,我的 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多字,希望这篇文章还是对读者们有帮助。最近也是经历了裁员,和找工作一系列的事情,小小吐槽以下,就业环境不容乐观,但是基本上看见这篇文章的读者,都是热爱技术的,多学点知识基本上储备量上去了,面试还是很容易通过的。

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

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

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

WebSocket 心跳机制 重连策略 实时通信 网络编程
相关文章