前端的WebSocket使用

111 阅读5分钟

什么是WebSocket

websocket是一种协议,设计用于提供低延迟,全双工和长期运行的连接

全双工: 通信的两个参与方可以同时发送和接收数据,不需要等待对方的响应或传输完成

优势:

1.双向实时通信:允许在单个,长时间的连接上进行双向实时通信。在需要快速实时更新的应用程序里,比http更加高效

2.降低延迟:链接一旦建立便会保持开放,数据可以在客户端和服务器之间以比http更低的延迟进行传输

3.更高效的资源利用: 可以减少重复请求和响应开销,因为他的连接只需要建立一次

心跳机制

为了保持websocket稳定的长连接,在连接建立之后,服务器和客户端之间通过心跳包来保持连接状态,防止连接因为长时间没有数据传输而被切断

心跳包是一个空的数据帧,由客户端和服务器端定期发一个数据帧,以确保双方之间的连接仍然有效

缺点

不提供加密功能:如果有安全上的需求,需要采用其他方式来确保安全性,如:SSL协议,设置黑白名单

不支持IE10以前的版本

优化很重要

简单示例

服务端
yarn add express
yarn add node-js-websocket

const socket = require('nodejs-websocket')
socket.createServer(function(conn){
    conn.on('text',function(str){
         conn.sendText(str)
    })
}).listen(8080)

前端
const socket = new WebSocket('ws://127.0.0.1:8080')
socket.onopen = function(){
    console.log('连接成功')
}
socket.onmessage = function(){
    console.log(e.data)
}
socket.onclose = function(){
    console.log('关闭连接')
}
btn1.addEventListener('click', function(){
    socket.send(one.value)
})
btn2.addEventListener('click', function(){
    socket.send(two.value)
})

心跳检测

const socket = new WebSocket('ws://your-server-url.com')
let heartbeatInterval;
const HEARTBEAT_INTERVAL = 30000

socket.onopen = () => {
   startHeartbeat()
}

socket.onmessage = (event) => {
    if(event.data === 'pong') {
        // 服务器响应心跳
    }
}

socket.onclose = () => {
    clearInterval(heartbeatInterval)
}

function startHeartbeat() {
    heartbeatInterval = setInterval(()=> {
      if (socket.readyState === WebSocket.OPEN) {
        socket.send('ping'); // 发送心跳包
      }
    },HEARTBEAT_INTERVAL)
}

使用stompjs

import { Client } from '@stomp/stompjs'

// 长连接是否建立
let isWSConnected = false
let wsClient: Client

/**
 * 初始化长连接
 * @param {boolean} isActive 是否激活连接
 */
export function init(isActive = true) {
  wsClient = new Client({
    brokerURL: 'ws://127.0.0.1:8080',
    heartbeatIncoming: 30000,
    heartbeatOutgoing: 30000,
  })

  wsClient.onConnect = function (frame) {
    console.log('connected***')
    isWSConnected = true
    mitt.emit('onconnect', {
      iFrame: frame,
      client: wsClient,
    })
  }

  wsClient.onStompError = function (frame) {
    mitt.emit('onerror', {
      client: wsClient,
    })
    console.warn('Stomp error')
  }

  /**
   * 主动关闭成功时回调
   * @param {object} frame
   */
  wsClient.onDisconnect = function (frame) {
    console.log('dis connect***')
    isWSConnected = false
    mitt.emit('ondisconnect', {
      iFrame: frame,
      client: wsClient,
    })
  }

  wsClient.onWebSocketClose = function (frame) {
    console.log('web socket close***')
    if (isWSConnected) {
      // 如果是已经建立连接的情况下,才触发关闭事件
      isWSConnected = false
      mitt.emit('onclose', {
        iFrame: frame,
        client: wsClient,
      })
    }
  }
  if (isActive) {
    connect()
  }
}

/**
 * 激活连接
 */
export function connect() {
  if (wsClient) {
    wsClient.activate()
  }
}

/**
 * 关闭连接
 */
export function disconnect() {
  if (wsClient) {
    wsClient.deactivate()
  }
}

// 初始化,激活连接
init()

/**
 * @param {*} callBack
 */
export function onConnect(callBack) {
  mitt.on('onconnect', callBack)
  if (isWSConnected) {
    // 如果已经建立连接,直接触发当前方法
    callBack({
      client: wsClient,
    })
  }
}

/**
 * @param {*} callBack
 */
export function offConnect(callBack: CallbackBase) {
  mitt.off('onconnect', callBack)
}

export unsubscribe(subscriber) {
    if(subscriber) {
        subscriber.unsubscribe()
    }
}

onConnect方法触发订阅,可以在别的模块引用,这样整个项目中只需要建立一个链接,即用即销毁
onConnect(setSubscribe)

function setSubscribe() {
    subscriber = client.subscribe(`/test`, (message) => {
    const res =JSON.parse(message)
  })
}

在onUnmounted阶段销毁
unsubscirbe(subscriber)
offConnect(setSubscribe)

使用原生webscoket封装

export const createSocket = async (options) => {
  if (!window.WebSocket) {
    console.log("您的浏览器不支持websocket")
    return
  }
  //类似mitt
  const dep = new EventMap()
  const oSocket = new JqSocket(options, dep)
  return oSocket
}

const EventTypes = ["open", "close", "message", "error"] //监听事件类型
const DEFAULT_CHECK_TIME = 55 * 1000 // 心跳检测的默认时间
const DEFAULT_CHECK_COUNT = 3 // 心跳检测默认失败重连次数
const DEFAULT_CHECK_DATA = { Type: 1, Parameters: ["alive"] } // 心跳检测的默认参数 - 跟后端协商的

class JqSocket extends WebSocket {
  heartCheckData = DEFAULT_CHECK_DATA
  heartCheckTimeout = DEFAULT_CHECK_TIME
  heartCheckInterval = null
  heartCheckCount = DEFAULT_CHECK_COUNT
  msgCacheQueue = [] // 待发消息缓存队列
  conversationId = undefined
  currentTime = null
  msgReceiveTaskTrigger = null //缓存触发器
  isEnd = false
  isOpen = false
  constructor(options, dep) {
    super(options.url)
    this._currentOptions = options
    this._dep = dep
    this.conversationId = options.conversationId
    this._init()
  }
  _init() {
    //重写原生webscoket方法
    this.onopen = this._handleOpen
    this.onclose = this._handleClose
    this.onerror = this._handleError
    this.onmessage = this._receiveMsg
    this._initMsgReceiveTaskTrigger()
  }
  //接受到的消息先缓存队列
 _initMsgReceiveTaskTrigger() {
    if (!this._currentOptions.isCache) return
    const { count, time } = this._currentOptions.optCacheReceive
    const cb = this._handleCacheResMsgs.bind(this)
    this.msgReceiveTaskTrigger = new TaskTrigger(count, time, cb)
    this.msgReceiveTaskTrigger.start()
 }
  _handleOpen(e) {
    this.isOpen = true
    this._dep.notify("open", e)
    this._handleMsgCacheQueue()
  }
  // 待发消息缓存
  _handleMsgCacheQueue() {
    if (this.msgCacheQueue.length > 0) {
      let msg = this.msgCacheQueue.shift()
      this.sendMessage(msg)
    }
    this.msgCacheQueue.length > 0 && this._handleMsgCacheQueue()
  }
  _checkFailConect() {
    if (this.isOpen) return
    //网络或者服务原因原因(识别链接没建立起来且有权限)
    notifyErr(SERVER_WEBSOCKET_ERR_TIP)
    this.isOpen = false
  }
  _handleClose(e) {
    this._checkFailConect()
    this._clearEffect()
    if (!this.isEnd) {
      this._handleError(
        Object.assign(Object.create(null), WS_ERR_MSG, {
          code: SERVER_WS_CODE.ERROR_NO_NORMAL_FLAG,
          msg: "未正常结束"
        })
      )
    }
    setTimeout(() => {
      this._dep.notify("close", {
        event: e,
        isEnd: this.isEnd,
      }) // wait:如果WebSocket是非正常关闭 则进行重连
    }, 400) //延时适配缓存区执行
  }
  _handleError(e) {
    this._dep.notify("error", e)
  }
  // 接收消息
  _receiveMsg(e) {
     if (!this._currentOptions.isCache) {
      this._handleMessage(e)
      return
    }
    this.msgReceiveTaskTrigger && this.msgReceiveTaskTrigger.cacheMessage(e)
  }
  _handleCacheResMsgs(msgArr = []) {
    let _this = this
    msgArr.forEach(msg => {
      _this._handleMessage(msg)
    })
  }
  _handleMessage(e) {
    try {
      let mapData = JSON.parse(e.data)
      const { code, data } = mapData
      if (code === SERVER_RES_CODE.SUCCESS_FINISH) {
        if (this.isEnd) return
        this.isEnd = true
      }
      if (code === SERVER_RES_CODE.LOGIN_FAILE) {
        // this.$message.error("用户信息已失效,请重新登录")
        window.location.href = "/login"
        return
      } else if (code !== SERVER_RES_CODE.SUC) {
        //请求失败
        this._handleError(mapData)
        return
      }
      // message是写死固定的,订阅的时候方法名称必须是message
      this._dep.notify("message", res) //业务侧只处理成功消息
    } catch (e) {
      console.error(e)
    }
  }
  // 订阅事件  此处注册监听
  subscribe(eventType, callback) {
    if (typeof callback !== "function")
    throw new Error("The second param is must be a function")
    this._dep.depend(eventType, callback)
  }
  // 取消订阅
  unSubscribe(eventType, callback) {
    if (typeof callback !== "function")
    throw new Error("The second param is must be a function")
    this._dep.unDepend(eventType, callback)
  }
   /**
   * 记录任务信息
   * @param {Object} data 任务消息
   */
  _recordTask(data) {
    //一次链接中,只更新一次currentTime
    if (isObject(data) && data.currentTime && !this.currentTime) {
      this.currentTime = data.currentTime
    }
  }
  // 发送消息
  sendMessage(data, options = {}) {
    const { transformJSON = true } = options
    let result = data
    if (transformJSON) {
      result = JSON.stringify(data)
    }
    if (this.readyState === this.OPEN) {
      this.send(result)
    } else if (this.readyState === this.CONNECTING) {
      // 第一次状态还没变成open,走这里存入队列,状态变成open后调用_handleMsgCacheQueue方法再sendMessage
      this.msgCacheQueue.push(result)
    } else {
      console.log("websocket关闭中或关闭状态")
    }
  }
  /**
   * 创建终止消息
   * @returns { msgStop }
   */
  _createStopMsg = () => {
    const { conversationId, currentTime } = this
    const msgStop = {
      status: 1,
      currentTime,
      conversationId
    }
    return msgStop
  }
  
   /**
   * 关闭WebSocket
   * @param {Object} param
   * @param { Number } [param.type = 1] 关闭类型 1-主动关闭
   * @param { Number } [param.code = 1000] 关闭码
   * @param { String } [param.reason = ''] 关闭原因
   */
  closeSocket(param) {
    const { type = 1, code = 1000, reason = "" } = Object.assign(
      Object.create(null),
      param
    )
    if (this.readyState !== this.OPEN || this.isEnd) {
      return
    }
    this.isEnd = true
    const { conversationId, currentTime } = this
    if (conversationId && currentTime) {
      this.sendMessage(this._createStopMsg())
    }
    this.close(code, reason) //todo: 关心跳,关链接,清定时器,清缓存
  }
  // 开始心跳检测
  heartCheckStart() {
    this.heartCheckInterval = setInterval(() => {
      if (this.readyState === this.OPEN) {
        let transformJSON = typeof this.heartCheckData === "object"
        this.sendMessage(this.heartCheckData, { transformJSON })
      } else {
        this.clearHeartCheck()
      }
    }, this.heartCheckTimeout)
  }
  // 清除心跳检测
  clearHeartCheck() {
    clearInterval(this.heartCheckInterval)
  }
  // 重置心跳检测
  resetHeartCheck() {
    clearInterval(this.heartCheckInterval)
    this.heartCheckStart()
  }
  _clearEffect() {
    if (this.msgReceiveTaskTrigger) {
      this.msgReceiveTaskTrigger.stop()
      this.msgReceiveTaskTrigger = null
    }
  }
}


使用
const jqSocket = await useJqSocket('url')
jqSocket.sendMessage(params)
jqSocket.subscribe("message", callback)  //注册message
jqSocket.subscribe("error", callback)
jqSocket.closeSocket()
// 封装工具函数
/**发布/订阅类 */
export class EventMap {
  deps = new Map()

  /**
   * 注册监听
   * @param {String} eventType
   * @param {Function} callback
   */

  depend(eventType, callback) {
    if (!this.deps.has(eventType)) {
      this.deps.set(eventType, [])
    }
    this.deps.get(eventType).push(callback)
  }

  /**
   * 取消监听
   * @param {String} eventType
   * @param {Function} callback
   */

  unDepend(eventType, callback) {
    if (this.deps.has(eventType)) {
      const callbacks = this.deps.get(eventType)
      const index = callbacks.findIndex(func => func === callback)
      if (index !== -1) {
        callbacks.splice(index, 1)
      }
    }
  }

  /**
   * 触发订阅事件
   * @param {String} eventType
   * @param {any} data
   */

  notify(eventType, data) {
    if (this.deps.has(eventType)) {
      const callbacks = this.deps.get(eventType)
      for (let callback of callbacks) {
        callback(data)
      }
    }
  }
}

export class TaskTrigger {
  constructor(msgCount, interval, triggerTask) {
    this.msgCount = msgCount
    this.interval = interval
    this.messages = []
    this.timer = null
    this.triggerTask = triggerTask
  }

  start() {
    this._resetTimer()
  }

  stop() {
    clearInterval(this.timer)
    this.timer = null
    this.triggerTask(this.messages)
    this.messages = []
  }

  _resetTimer() {
    this.timer && clearInterval(this.timer)
    this.timer = setInterval(() => {
      this.triggerTask(this.messages)
      this.messages = []
    }, this.interval)
  }

  cacheMessage(message) {
    this.messages.push(message)
    if (!this.msgCount) return
    // 判断消息条数是否足够
    if (this.messages.length >= this.msgCount) {
      // 执行任务,清空消息队列并重置定时器
      this.triggerTask(this.messages)
      this.messages = []
      this._resetTimer()
    }
  }
}