完美webSocket的封装及使用

首先介绍下webSocket,以及为什么要使用心跳检测和短线重连机制(代码在最下方)

WebSocket协议是基于TCP协议上的独立的通信协议,在建立WebSocket通信连接前,需要使用HTTP协议进行握手,从HTTP连接升级为WebSocket连接。

浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。 一旦WebSocket连接建立,服务器和客户端就可以通过发送消息来进行实时通信。这种双向通信机制。

Websocket 协议与 HTTP 协议没有关系,它是一个建立在 TCP 协议上的全新协议,为了兼容 HTTP 握手规范,在握手阶段依然使用 HTTP 协议,握手完成之后,数据通过 TCP 通道进行传输。

Websoket 数据传输是通过 frame 形式,一个消息可以分成几个片段传输。这样大数据可以分成一些小片段进行传输,不用考虑由于数据量大导致标志位不够的情况。也可以边生成数据边传递消息,提高传输效率。

WebSocket定义了两种URI格式, ws://wss:// ,类似于 HTTP 和 HTTPS , ws:// 使用明文传输,默认端口为80,wss:// 使用TLS加密传输,默认端口为443。

特点

  • 双向通信:服务器和客户端可以通过发送消息进行实时双向通信。

  • 持久连接:WebSocket连接是持久的,不需要在每次通信时重新建立连接。

  • 低开销:与传统的HTTP请求相比,WebSocket通信的开销较低,因为不需要频繁地建立和关闭连接。

  • 低延迟:由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少。

  • 支持跨域:WebSocket协议支持跨域通信,可以在不同域名或端口之间进行通信。

  • 减少数据传输:由于WebSocket是基于消息的,而不是基于请求/响应的,因此可以减少不必要的数据传输,从而提高传输效率。

优缺点

优点:
双向通信。客户端和服务端双方 都可以主动发起通讯。 没有同源限制。客户端可以与任意服务端通信,不存在跨域问题。 数据量轻。第一次连接时需要携带请求头,后面数据通信都不需要带请求头,减少了请求头的负荷。 传输效率高。因为只需要一次连接,所以数据传输效率高。

缺点:
长连接需要后端处理业务的代码更稳定,推送消息相对复杂; 兼容性,WebSocket 只支持 IE10 及其以上版本。 服务器长期维护长连接需要一定的成本,各个浏览器支持程度不一; 【需要后端代码稳定,受网络限制大,兼容性差,维护成本高,生态圈小】

使用场景

  1. 协同编辑

    • 场景描述:WebSocket可用于实现多人协同编辑,如在线文档协作、团队代码编辑等。
    • 实际应用:多个用户可以同时编辑同一个文档或代码文件,他们的编辑结果会实时地同步到其他用户的界面上。
      .
  2. 实时监控

    • 场景描述:WebSocket适用于实时监控系统,如监控设备的运行状态、实时监测交通流量等。
    • 实际应用:服务器可以实时地将监控数据推送给客户端,客户端可以及时地显示最新的监控信息。
      .
  3. 实时聊天

    • 场景描述:WebSocket是即时通讯的理想选择,如在线聊天室、多人游戏等。
    • 实际应用:客户端和服务器可以实时地发送和接收消息,无需频繁地发起HTTP请求。
      .
  4. 实时数据更新

    • 场景描述:WebSocket能够实时推送数据更新,如实时股票行情、实时天气预报等。
    • 实际应用:服务器可以实时地将最新的数据推送给客户端,客户端无需主动发起请求。
      .
  5. 游戏开发

    • 场景描述:WebSocket在游戏开发中具有重要作用,特别是在多人在线游戏中。
    • 实际应用:
      • 多人游戏协同:允许多个客户端同时连接到服务器,使多人协同游戏变得容易。
      • 实时聊天:游戏内的实时聊天变得非常容易实现。
      • 服务器推送:服务器可以主动推送消息给客户端,降低了服务器和网络的负担。
      • 实时排行榜和统计:游戏服务器可以实时更新排行榜和统计信息。
      • 游戏状态同步:通过建立持久的WebSocket连接,游戏服务器可以实时广播游戏状态的更新。

  • 创建了一个新的WebSocket对象,并指定了要连接的WebSocket服务器URL

  • 为WebSocket对象添加了几个事件监听器,以处理连接建立、接收到消息、发生错误和连接关闭等事件:

    • onopen:在连接建立时触发,你可以在这个事件处理函数中发送初始消息

    • onmessage:在接收到消息(来自服务器)时触发,你可以在这个事件处理函数中处理接收到的消息

    • onerror:在发生错误时触发

    • onclose:在连接关闭时触发

  • sendMessage函数是一个简单的封装,用于在WebSocket连接打开时发送消息。如果连接未打开,它会打印一条消息到控制台。

  • 最后,调用 socket.close() 来手动关闭 WebSocket 连接。但通常,当不再需要 WebSocket 连接时,或者当服务器关闭连接时,连接会自动关闭。


封装

封装一个支持断网重连心跳检测功能,并且兼容原生WebSocket写法的JavaScript WebSocket类

断网重连

WebSocket 断网重连(Reconnect after Network Disconnection)是指在 WebSocket 连接因为网络问题(如网络不稳定、临时断网、服务器宕机等)而断开后,客户端能够自动检测到连接已断开,并在网络恢复后尝试重新建立与服务器的 WebSocket 连接的过程。

在网络通信中,由于各种不可控因素,WebSocket 连接可能会意外断开。为了确保服务的连续性和可用性,很多 WebSocket 客户端库或框架都会提供断网重连的功能。

  1. 连接状态监听:客户端需要监听 WebSocket 的 onclose 事件,该事件会在连接关闭时被触发。一旦接收到 onclose 事件,客户端就知道连接已经断开。

  2. 重连策略:在连接断开后,客户端会根据预定义的重连策略来决定是否尝试重新连接。重连策略可以包括重连间隔、重连次数限制等。例如,客户端可能会等待一段时间后再次尝试连接,如果连接仍然失败,它会继续等待更长的时间再次尝试,直到达到最大重连次数或用户干预。

  3. 重连实现:在决定重连后,客户端会重新调用 WebSocket 的构造函数或相关方法,以尝试与服务器建立新的 WebSocket 连接。如果连接成功,客户端会恢复正常的通信。

  4. 心跳检测:为了更准确地检测连接状态,很多 WebSocket 客户端还会实现心跳检测机制。客户端定期向服务器发送心跳消息,如果服务器在规定的时间内没有响应,客户端就认为连接已经断开,并开始执行重连逻辑。

  5. 日志和通知:在重连过程中,客户端可能会记录日志以便后续分析,并在需要时向用户或开发者发送通知。

心跳检测

WebSocket 心跳(Heartbeat)是指为了在 WebSocket 连接中检测连接的活跃性和可用性而定期发送的简短消息。WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,允许服务器主动向客户端推送信息,客户端也可以主动向服务器发送信息。

由于网络环境的复杂性,如网络不稳定、防火墙或代理服务器配置不当等原因,WebSocket 连接有时可能会“假死”,即连接仍然处于打开状态(readyState 为 OPEN),但实际上数据已经无法传输。在这种情况下,如果没有任何数据在连接上传输,双方都无法得知连接是否已经失效。

为了解决这个问题,可以定期发送心跳消息来检测连接的活跃性。心跳消息通常是一个简短的数据包,例如一个简单的字符串或数字。客户端和服务器都可以发送心跳消息,但通常是由客户端发送,因为服务器可以更容易地检测和管理多个连接。

当接收到心跳消息时,接收方会重置其心跳定时器,并知道连接仍然是活跃的。如果在指定的时间间隔内没有收到心跳消息,接收方可以认为连接已经失效,并采取相应的措施,如关闭连接、尝试重新连接或通知用户。

  1. 发送频率:心跳消息的发送频率应该根据具体的应用场景和网络环境来确定。过于频繁的心跳消息可能会增加网络负担,而发送间隔过长则可能无法及时发现连接问题。

  2. 消息内容:心跳消息的内容应该简短,以减少网络带宽的占用。通常,一个简单的字符串或数字就足够了。

  3. 处理接收到的消息:当接收到心跳消息时,接收方应该重置其心跳定时器,并继续等待下一个心跳消息。如果接收到非心跳消息,则应该根据应用的逻辑来处理。

  4. 重连机制:如果心跳检测发现连接已经失效,接收方应该尝试重新建立连接。重连的间隔和次数可以根据需要来配置。

主要封装如下:封装成class类,下列代码使用了TypeScript封装。可直接cv使用。

创建ts文件,或者你是js(进行简单改装,删掉不必要的类型约定即可)。放到hooks或者是项目集成文件目录下即可。
// import { getUUID } from '@ranger/utils'
export type zWebSocket = WebSocket & {
  tag: string
}

export interface ISocketMsg {
  type: string
  data: any
}

export const enum SocketState {
  'PENDING' = 1, // 未连接
  'CONNECTED', // 已连接
  'RECONNECTING', // 再次连接中
  'CONNECTION_FAILED', // 连接失败
}

const wsMap: {
  [key: string]: CreateWebSocket
} = {}

window.wsMap = wsMap

export class CreateWebSocket {
  readonly wsUrl: string

  readonly tag: string

  readonly usetId: number

  reconnectTime: number | 'Infinite' // 重试连接次数

  reconnectIntervalTime: number // 重试连接间隔时间

  reconnectCb: (data?: any) => void // 重试连接成功后的操作

  #socket: zWebSocket | null = null

  #state = SocketState.PENDING

  #lockReconnect = false

  #num = 0 // 当前重试次数

  readonly MAX_CONNECT_TIME = 4 // 最大重试连接次数

  readonly timeout = 5000 // 每隔五秒发送心跳

  readonly severTimeout = 5000 // 服务端超时时间

  #clientTimer: null | NodeJS.Timeout = null

  #serverTimer: null | NodeJS.Timeout = null

  #connectTimer: null | NodeJS.Timeout = null

  readonly cbMap: { [key: string]: (data: ISocketMsg) => void } = {}

  /**
   * Creates an instance of CreateWebSocket.
   * @param {string} wsUrl socket链接路径
   * @param {string} tag 链接标识符
   * @param {number} usetId 登录人id
   * @param {number} reconnectTime
   * @param {string} requestParameters 请求入参
   * @param {number} reconnectIntervalTime 重试连接间隔时间
   * @param {(data?: any)=>void} reconnectCb 重试连接间隔时间
   * @memberof CreateWebSocket
   */
  constructor(
    wsUrl: string,
    tag: string,
    // eslint-disable-next-line @typescript-eslint/default-param-last
    usetId: number,
    reconnectTime: number | 'Infinite' = 4,
    requestParameters?: string,
    reconnectIntervalTime: number = 5000,
    reconnectCb = () => {}
  ) {
    this.wsUrl = `${wsUrl}?type=${tag}&user=${usetId || -1}${
      requestParameters || ''
    }`
    this.tag = tag
    this.usetId = usetId
    this.reconnectTime = reconnectTime
    this.reconnectIntervalTime = reconnectIntervalTime
    this.reconnectCb = reconnectCb

    this.connect()
  }

  connect() {
    // 已连接或正在重连的时候不做处理
    // TODO:正在重连 RECONNECTING状态 的时候是否不做处理?
    if (
      this.#state === SocketState.CONNECTED ||
      this.#state === SocketState.RECONNECTING
    ) {
      return
    }
    // TODO:如果 正在重连 RECONNECTING状态 的时候可以重新连接那么下面这条判断就需要放开
    /* if (this.#connectTimer) {
      clearTimeout(this.#connectTimer)
    } */
    try {
      this.#socket = new WebSocket(this.wsUrl) as zWebSocket
      this.#socket.tag = this.tag // 设置标签
      this.init()
    } catch (error) {
      console.log(error)
    }
  }

  init() {
    this.#socket!.onopen = () => {
      this.#num = 0 // 连接成功后将当前重试次数重置为0,以便异常断开后能从0尝试重连至最大重试连接次数
      this.reconnectCb && this.reconnectCb() // 连接成功后 的回调
      wsMap[this.tag] = this
      this.#state = SocketState.CONNECTED
      if (this.#socket!.readyState === 1) {
        // if (this.wsUrl.split('/')[6] === '1') {
        //   this.send({ data: 'P', type: 'ping' })
        // } else {
        //   this.send({ data: this.roleNames, type: 'roles' })
        // }
        this.send({ data: 'P', type: 'ping' })
      }
      // 心跳检测重置
      this.heartCheck()
    }

    this.#socket!.onclose = (e: any) => {
      console.warn('#socket.onclose', this.#socket)
      this.close()
      if (e.code !== 1000) {
        this.reconnect()
      }
    }

    this.#socket!.onerror = () => {
      // console.error('#socket.onerror', this.#socket)
    }

    this.#socket!.onmessage = (event: any) => {
      // 拿到任何消息都说明当前连接是正常的
      this.heartCheck()

      const data: ISocketMsg = JSON.parse(event.data)

		// 这里是按照项目的数据结构处理的,   可以跟换成你们约定的数据结构(回调)
      Object.entries(this.cbMap).forEach(([type, fun]) => {
        if (data.type !== 'ping' && data.data) {
          fun({
            data: JSON.parse(data.data),
            type: data.type,
          })
        }
      })
    }
  }

  setSocket() {
    return this.#socket
  }

  getState() {
    return this.#state
  }

  // 添加回调
  addCb(type: string, fun: (data: ISocketMsg) => void) {
    this.cbMap[type] = fun
  }

  getCbMap() {
    return this.cbMap
  }

  // 心跳检测
  heartCheck() {
    if (this.#clientTimer) {
      clearTimeout(this.#clientTimer)
    }
    if (this.#serverTimer) {
      clearTimeout(this.#serverTimer)
    }

    this.#clientTimer = setTimeout(() => {
      // 这里发送一个心跳,后端收到后,返回一个心跳消息,
      // onmessage拿到返回的心跳就说明连接正常
      if (this.#socket?.readyState === 1) {
        this.send({ data: 'P', type: 'ping' }) // 心跳包
      }
      // 计算答复的超时时间
      this.#serverTimer = setTimeout(() => {
        this.close()
      }, this.severTimeout)
    }, this.timeout)
  }

  send(data: object) {
    // const tflag = getUUID(16)
    this.#socket!.send(JSON.stringify(data))
  }

  // 重连操作,通过设置lockReconnect变量避免重复连接
  reconnect() {
    if (this.#lockReconnect) {
      this.#state = SocketState.CONNECTION_FAILED
      return
    }
    this.#state = SocketState.RECONNECTING
    this.#lockReconnect = true
    // 没连接上会连接this.reconnectTime次,从此不在连接(刷新页面重连)
    if (this.reconnectTime === 'Infinite' || this.#num < this.reconnectTime) {
      this.#lockReconnect = false
      this.#num += 1
      this.#connectTimer = setTimeout(() => {
        this.connect()
      }, this.reconnectIntervalTime)
    }
  }

  close() {
    if (this.#socket) {
      this.#socket!.close()
      Reflect.deleteProperty(wsMap, this.tag)
      this.#socket = null
      this.#state = SocketState.PENDING
    }
  }
}

/**
 * 关闭socket
 * @param {string} [tag] 关闭的ws标识
 */
export const closeWebSocket = (tag?: string) => {
  try {
    if (tag) {
      wsMap[tag] && wsMap[tag].close()
    } else {
      // 关闭所有socket
      Object.values(wsMap).forEach((ws) => {
        ws.close()
      })
    }
  } catch (err) {
    console.log(err)
  }
}

具体使用实例:

// 这里引入封装好的文件 导入
import { CreateWebSocket, ISocketMsg } from '@/hooks/socket/Socket'

// wsUrl:可以从你链接socket的地方约定url (ws 或者是 wss),相当于一个参数传递进来
export default function useInitYcsxSocket(wsUrl: string) {

  const ws = new CreateWebSocket(
    wsUrl,  // 也可在这里直接写你需要链接的 url链接 (ws 或者是 wss)
    'HwsSocketMsg', // 这里是针对这个 socket 的特定标识  后续关闭socket使用等
    homeStore.loginData.idCard,  // 这里可不传值,我这里传值是因为项目需要
    'Infinite',  // socket 重连次数,这里Infinite表示无限重连   可以为数字:1 2 3等
    {}, // 这里是传递其它参数  看约定参数等
  )

// socket 发送消息后的回调
  ws.addCb('yczh', (data: any) => {
    // info 拿到发送过来的数据
    const info = data.data
   // 这里进行你的操作
  })
}

断开socket连接:

import { closeWebSocket } from '@/hooks/socket/Socket'

// 参数 socket标识
closeWebSocket('HwsSocketMsg')

注意

  • WebSocket连接可以使用安全的WebSocket协议(wss://)进行保护,它使用SSL/TLS添加了额外的加密层。这确保了在客户端和服务器之间传输的数据的机密性和完整性。

  • WebSocke t是一种基于TCP的协议,支持长连接。在实际使用中,需要注意及时维护长连接,避免因连接长时间不活跃而被网络设备断开。为了确保长连接的稳定性,可以定时发送心跳包,以保持连接的活跃状态。

  • 大多数现代Web浏览器都支持WebSocket协议,包括Chrome、Firefox、Safari和Edge。然而,需要考虑WebSocket在旧浏览器或支持有限的环境中的兼容性。在这种情况下,可以使用回退机制。

以上就是整体的封装及使用,对你有用的话,点赞收藏起来吧!

在 Vue 中使用 WebSocket 需要进行如下步骤: 1. 安装 `websocket` 库 ```bash npm install websocket ``` 2. 封装 WebSocket 在 Vue 项目中,通常我们会将 WebSocket 封装成一个模块,方便其他组件调用。以下是一个示例: ```javascript import WebSocket from 'websocket'; class WebsocketClient { constructor(url) { this.url = url; this.client = null; this.connected = false; this.listeners = {}; this.connect(); } connect() { this.client = new WebSocket.client(); this.client.on('connect', () => { this.connected = true; this.emit('connect'); }); this.client.on('message', (message) => { this.emit('message', message); }); this.client.on('close', () => { this.connected = false; this.emit('close'); }); this.client.connect(this.url); } send(data) { if (this.connected) { this.client.send(JSON.stringify(data)); } } on(event, listener) { this.listeners[event] = this.listeners[event] || []; this.listeners[event].push(listener); } emit(event, ...args) { const listeners = this.listeners[event]; if (listeners) { listeners.forEach((listener) => { listener.apply(null, args); }); } } close() { this.client.close(); } } export default WebsocketClient; ``` 3. 在组件中使用 WebSocket 在需要使用 WebSocket 的组件中使用上面封装好的 WebSocket 模块。例如,在 `App.vue` 组件中使用: ```vue <template> <div class="app"> <h1>Hello, WebSocket!</h1> </div> </template> <script> import WebsocketClient from './websocket'; export default { name: 'App', created() { this.websocket = new WebsocketClient('ws://localhost:8080'); this.websocket.on('connect', () => { console.log('WebSocket connected!'); }); this.websocket.on('message', (message) => { console.log('WebSocket message received:', message); }); this.websocket.on('close', () => { console.log('WebSocket closed!'); }); }, beforeDestroy() { this.websocket.close(); }, }; </script> ``` 在上面的代码中,我们在 `created()` 生命周期中创建了一个 WebSocket 实例,并绑定了 `connect`、`message`、`close` 事件。在组件销毁之前,我们需要调用 `close()` 方法关闭 WebSocket 连接,防止内存泄漏。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值