封装websocket,实现 (队列形式,心跳检测,断线重连,离线数据重发)

前言

WebSocket 是一种常见的实时通信方案。它可以用于实现即时聊天、推送消息、在线状态同步等功能。然而,WebSocket 连接容易受到网络环境的影响,可能会断开,因此需要一种自动重连的机制。本文介绍一个基于 TypeScript 的 WebSocket 封装类 Ws,它具备自动重连、消息缓存、心跳检测、错误处理等功能。使用到技术websocket,worker。

worker.ts

ts 复制代码
self.onmessage = function (event) {
  /// 收到clear消息后, 清除定时器
  if (event.data === 'clear') {
    clearInterval(self.intervalID)
  } else {
    const delay = event.data
    self.intervalID ||= setInterval(() => {
      self.postMessage({}) // 定时通知主线程,即上文中的 xxx
    }, delay)
  }
}

websock.ts

ts 复制代码
/**
 * WebSocket.CONNECTING === 0
 * WebSocket.OPEN === 1
 * WebSocket.CLOSING === 2
 * WebSocket.CLOSED === 3
 */
type MessageData = {
  type: number // 1:心跳 2:单聊 3:群聊 4:系统消息
  data: any // 消息内容
  sender: string // 发送者
  receiver: string // 接收者 
}
type WebsocketExample = {
  name: string
  url: string
  socket: WebSocket
  isActiveClose: boolean
  reconnectCount: number
}
type ConnectConfig = {
  name: string
  url: string
}
const excludeKey = ['onMessage', 'onError',   'sockteManager', 'messagePipe',"worker","isNetwork"]
/**
 * 1006(异常关闭,CLOSE_ABNORMAL):​表示连接异常关闭,通常由于网络问题或服务器崩溃等原因导致。由于未发送关闭帧,客户端无法获知具体原因,因此需要尝试重新连接。​
 * 1001(终端离开,CLOSE_GOING_AWAY):​表示服务器或客户端由于关闭、重启等原因主动断开连接。此时,客户端可以尝试重新建立连接。​
 * 1005(无状态码,CLOSE_NO_STATUS):​表示未收到预期的关闭状态码,可能由于连接中断等原因导致。客户端应考虑重新连接。
 */
const errorCodes = [1006, 1001, 1005]
const pingMsg:MessageData = {
  type: 1,
  data: '',
  sender: '',
  receiver: ''
}
class Ws {
  name: string = crypto.randomUUID()
  baseUrl: string
  header: string[] 
  reconnectCount: number = 3
  heartbeatSendTime: number = 60000
  reconnectTimeInterval: number = 5000
  private readonly onMessage: Map<string, Function> = new Map<string, Function[]>()
  private readonly onError: Map<string, Function> = new Map<string, Function[]>()
  private readonly sockteManager: Map<string, WebsocketExample> = new Map<string, WebsocketExample>()
  private readonly messagePipe: Map<string, MessageData[]> = new Map<string, MessageData[]>()
  private readonly worker: Worker
  private readonly isNetwork: boolean = false
  constructor(config) {
    for (let key in config) {
      if (!excludeKey.includes(key)) {
        this[key] = config[key]
      }
    }
  }
  getWsContainer() {
    return this.sockteManager
  }

  addCheckMessage(name: string, func: Function) {
    if (!this.onMessage.has(name)) {
      this.onMessage.set(name, [])
    }
    this.onMessage.get(name).push(func)
  }
  addCheckError(name: string, func: Function) {
    if (!this.onError.has(name)) {
      this.onError.set(name, [])
    }
    this.onError.get(name).push(func)
  }
  // 发送消息
  send(name: string, msg: MessageData) {
    
    // 如果不存在直接把数据存到管道
    if (!this.sockteManager.has(name)) {
      this.setMessagePipe(name, msg)
      return
    }
    let wsExample = this.sockteManager.get(name)
    // 如果连接成功,直接发送
    if (wsExample.socket.readyState === WebSocket.OPEN) {
      wsExample.socket.send(JSON.stringify(msg))
      return
    }
    // 如果连接失败,把数据存到管道(等待重新连接的时候再发送)
    this.setMessagePipe(name, msg)
  }

  private readonly setMessagePipe = (name: string,msg: MessageData) => {
    if (!this.messagePipe.has(name)) {
      this.messagePipe.set(name, [])
    }
    this.messagePipe.get(name).push(msg)
  }
  // 连接
  connect(config: ConnectConfig): string {
    const { name, url } = config
    if (!name || !url) {
      throw new Error('ws 名称和url不能为空')
      return
    }
    let wsUrl
    if (this.baseUrl) {
      wsUrl = this.baseUrl + url
    }
    let ws = new WebSocket(wsUrl,this.header)
    let myWs: WebsocketExample = {
      name,
      url,
      socket: ws,
      isActiveClose: false,
      reconnectCount: this.reconnectCount,
    }
    if (!this.sockteManager.has(name)) {
      this.sockteManager.set(name, myWs)
    } else { 
      myWs.reconnectCount = this.sockteManager.get(name).reconnectCount
      this.sockteManager.set(name, myWs)
    }

    ws.onopen = () => {
      console.log(`${name} 连接成功`, this)
      this.resetReconnectCount(name) // 重置重连次数 
      // 判断是否开启网络检测 心跳检测
      if (!this.isNetwork) { 
        this.start() // 开启网络检测
      }
      if (this.messagePipe.has(name)) {  // 如果管道有数据,则发送
        this.messagePipe.get(name).forEach((msg) => {
          this.send(name, msg)
        })
      }
      
    }
    ws.onmessage = (msg) => {
      if (this.onMessage.has(name)) {
  
        this.onMessage.get(name).forEach((f) => {
          f(JSON.parse(msg.data))
        })
      }
    }
    ws.onerror = (error) => {
      if (this.onError.has(name)) { 
        this.onError.get(name).forEach((f) => { 
          f(error)
        })
      }
      
    }
    ws.onclose = (event) => {
      if (!errorCodes.includes(event.code)) return
      let wsExample = this.sockteManager.get(name)
      if (!this.isNetwork) {
        throw new Error('网络异常')
        return
      }
      if (wsExample.isActiveClose) {
        this.deleteMap(name)
        return
      }
      if (wsExample.socket.readyState === WebSocket.CLOSED && wsExample.reconnectCount > 0) { 
        this.reconnect(name)
      }
    }
    return name
  }
  // 关闭
  close(name) {
    if (this.sockteManager.has(name)) {
      let wsExample = this.sockteManager.get(name)
      wsExample.isActiveClose = true
      wsExample.socket.close()
    }
   
  }
  private readonly start() {
    this.startNetwork()
      this.startWroker()
    
  }
  private readonly deleteMap(name) {
    this.sockteManager.delete(name)
    this.onMessage.delete(name)
    this.onError.delete(name)
    if (this.sockteManager.size === 0) {
      this.stopNetwork() // 当没有socket连接的时候,关闭网络检测
      this.closeWroker() // 关闭心跳检测
     }
  }
  // 重连
  private readonly reconnect(name) {
    let wsExample = this.sockteManager.get(name)
    if (wsExample.reconnectCount <= 0) {
      this.deleteMap(name)
      throw new Error('重连失败')
      return
    }
    setTimeout(() => {
      --wsExample.reconnectCount
      this.connect({ name: wsExample.name, url: wsExample.url })
    }, this.reconnectTimeInterval)
  }
  private readonly resetReconnectCount(name) {
    let socket = this.sockteManager.get(name)
    socket.reconnectCount = this.reconnectCount
    this.sockteManager.set(name, socket)
  }
  private readonly startHeartbeat() {
    this.sockteManager.forEach((wsExample) => {
      if (wsExample.socket.readyState === WebSocket.OPEN) {
        wsExample.socket.send(JSON.stringify(pingMsg))
       }
    })
  }
  private readonly onNetwork=()=> {
    // 开启网络检测
    if (navigator.onLine) {
      this.startWroker()
      // 循环重连
      this.sockteManager.forEach((wsExample) => {
        if (wsExample.socket.readyState != WebSocket.OPEN) { 
          this.reconnect(wsExample.name)
        }
      })
    } else {
      this.closeWroker()
    }
    this.isNetwork = navigator.onLine
  }
  private readonly startNetwork() {
    if (!this.isNetwork) {
      this.isNetwork = true
      window.addEventListener('online', this.onNetwork)
      window.addEventListener('offline', this.onNetwork)
    }
  }
  private readonly stopNetwork() {
    if (this.isNetwork) {
      window.removeEventListener('online', this.onNetwork)
      window.removeEventListener('offline', this.onNetwork)
      this.isNetwork = false
    }
  }
  private readonly startWroker() {
    this.worker = new Worker('/worker.ts')
    this.worker.postMessage(this.heartbeatSendTime) // 传递 delay 延时参数

    // 接收 Web Worker 的消息
    this.worker.onmessage = (event) => {
      this.startHeartbeat()
    }
  }
  private readonly closeWroker() {
    this.worker.postMessage('clear')
    this.worker.terminate()
  }
}

export default Ws

使用

ts 复制代码
import ws from "@/utils/socket/socket"

const wsEx = new ws({ baseUrl:"ws://127.0.0.1:9898",header:["token"] });

// 建立连接
const name = wsEx.connect({ name: "xwya", url: "/api/system/webws/xwya" })

// 检测数据
wsEx.addCheckMessage(name,(msg) => {
    console.log(msg);
})
// 关闭
wsEx.close( name)

// 发送数据
w1.send(name,msg)
相关推荐
大土豆的bug记录3 小时前
鸿蒙进行视频上传,使用 request.uploadFile方法
开发语言·前端·华为·arkts·鸿蒙·arkui
maybe02093 小时前
前端表格数据导出Excel文件方法,列自适应宽度、增加合计、自定义文件名称
前端·javascript·excel·js·大前端
HBR666_3 小时前
菜单(路由)权限&按钮权限&路由进度条
前端·vue
A-Kamen3 小时前
深入理解 HTML5 Web Workers:提升网页性能的关键技术解析
前端·html·html5
锋小张5 小时前
a-date-picker 格式化日期格式 YYYY-MM-DD HH:mm:ss
前端·javascript·vue.js
鱼樱前端5 小时前
前端模块化开发标准全面解析--ESM获得绝杀
前端·javascript
yanlele5 小时前
前端面试第 75 期 - 前端质量问题专题(11 道题)
前端·javascript·面试
前端小白۞6 小时前
el-date-picker时间范围 编辑回显后不能修改问题
前端·vue.js·elementui
拉不动的猪6 小时前
刷刷题44(uniapp-中级)
前端·javascript·面试
Spider Cat 蜘蛛猫7 小时前
chrome插件开发之API解析-chrome.scripting.executeScript()
前端·chrome