WebSocket实现直播弹幕滚动推送效果

WebSocket + 弹幕滚动推送

      • [WebSocket 通信协议优点](#WebSocket 通信协议优点)
      • 实现过程详细解析
        • [1. 初始化 WebSocket 连接](#1. 初始化 WebSocket 连接)
        • [2. WebSocket 事件回调](#2. WebSocket 事件回调)
        • [2.2 连接错误 (onerror)](#2.2 连接错误 (onerror))
        • [2.3 接收到消息 (onmessage)](#2.3 接收到消息 (onmessage))
        • [2.4 连接关闭 (onclose)](#2.4 连接关闭 (onclose))
        • [3. 心跳检测机制](#3. 心跳检测机制)
        • [4. WebSocket 重新连接机制](#4. WebSocket 重新连接机制)
        • [5. 滚动加载和历史数据](#5. 滚动加载和历史数据)
      • 总结
      • 代码示例

WebSocket 通信协议优点

WebSocket 是一种通信协议,它提供了一个全双工、低延迟的通信通道,允许客户端和服务器之间进行实时的数据交换。它比传统的轮询请求方式更加高效,因为它建立在持久连接之上,而不需要每次都重新建立连接。使用 WebSocket,可以实现实时数据推送、在线聊天、消息通知等功能。

双向通信: WebSocket 允许客户端和服务器之间进行双向通信,这意味着双方都可以主动发送消息,而不仅仅是客户端向服务器发送请求。
降低延迟: 相较于传统的 HTTP 协议,WebSocket 在连接建立后能保持一个持久连接,从而减少了每次请求和响应之间的延迟。
节省带宽: WebSocket 使用一种轻量级的帧格式,相比于 HTTP,相同的数据传输量能够消耗更少的带宽和资源。
实时性: WebSocket 特别适合需要实时数据更新的应用,比如在线游戏、即时聊天应用、实时股票更新或体育赛事直播。
简单的 API: WebSocket 提供了易用的 JavaScript API,使得开发人员能够轻松实现复杂的实时通信功能。

适用场景:

  • 在线游戏:需要快速、安全、实时的数据传输。
  • 聊天应用:实现实时的消息推送。
  • 股票市场:实时更新数据的应用程序。
  • 协作工具:如在线文档编辑,允许多用户实时便捷地进行协作。

实现过程详细解析

详细解析在 Vue 代码中 WebSocket实现直播弹幕滚动效果过程,逐步说明各个方法和关键部分。

1. 初始化 WebSocket 连接
javascript 复制代码
setupWebSocket() {
  const env = process.env.VUE_APP_ENV
  const urls = {
    prod: 'wss://domain.name/net/websocket/',
    test: 'wss://domaintest.name/net/websocket/',
    dev: 'ws://127.0.0.0:8080/net/websocket/'
  }
  const wsUrl = urls[env] || urls.dev
  if ('WebSocket' in window) {
    this.websocket = new WebSocket(`${wsUrl}${this.deptId}`)
    this.websocket.onopen = this.handleWebSocketOpen
    this.websocket.onerror = this.handleWebSocketError
    this.websocket.onmessage = this.handleWebSocketMessage
    this.websocket.onclose = this.handleWebSocketClose
  } else {
    alert('WebSocket not supported')
    this.reconnectWebSocket()
  }
}

环境配置: 通过 process.env.VUE_APP_ENV 获取当前应用的环境,并为不同环境(生产、测试、开发)指定 WebSocket 服务的 URL。

  • wss:// 是 WebSocket 的安全协议,类似于 HTTPS。
  • ws:// 是非加密的 WebSocket协议,通常在开发时使用。

WebSocket 实例化: 通过 new WebSocket(url) 创建 WebSocket 实例,连接到指定的 URL。URL 中动态传入 deptId(部门ID)来建立连接。

事件监听: 设置了 4 个 WebSocket 事件回调:

  • onopen:连接成功后触发,调用 handleWebSocketOpen。
  • onerror:连接出错时触发,调用 handleWebSocketError。
  • onmessage:接收到消息时触发,调用 handleWebSocketMessage。
  • onclose:连接关闭时触发,调用 handleWebSocketClose。

浏览器兼容性检查: 通过 'WebSocket' in window 判断当前浏览器是否支持 WebSocket。如果不支持,则弹出提示并尝试重连。

2. WebSocket 事件回调

连接成功 (onopen)

javascript 复制代码
handleWebSocketOpen() {
  this.heartCheck.start()
  console.log('WebSocket connection established.')
}
  • 当 WebSocket 连接成功时,调用 handleWebSocketOpen。在这里,调用了 heartCheck.start()
    来启动心跳检测,保持连接的持续性。
  • 控制台打印 WebSocket connection established. 来表明连接成功。
2.2 连接错误 (onerror)
javascript 复制代码
handleWebSocketError(error) {
  if (!this.beforeDestroyLeave) this.reconnectWebSocket()
  console.error('WebSocket error:', error)
}
  • 当 WebSocket 连接发生错误时,调用 handleWebSocketError。此时,尝试重新连接 WebSocket (this.reconnectWebSocket())。并在控制台输出错误信息。
2.3 接收到消息 (onmessage)
javascript 复制代码
handleWebSocketMessage(event) {
  this.heartCheck.reset().start()
  const data = JSON.parse(event.data)
  if (data.code === '心跳检测成功') return
  this.websocketData.unshift(data)
  this.updateHistoryData()
}
  • 当 WebSocket 收到消息时,调用 handleWebSocketMessage。
  • 通过 this.heartCheck.reset().start() 重置并重新启动心跳检测。
  • 将服务器返回的数据 event.data 解析成 JSON 格式(假设返回的消息是 JSON 字符串)。
  • 如果接收到的消息是"心跳检测成功"的响应,则跳过进一步处理。
  • 将接收到的数据 data 插入到 this.websocketData 数组的开头,并调用 updateHistoryData()
    更新历史数据展示。
2.4 连接关闭 (onclose)
javascript 复制代码
handleWebSocketClose() {
  if (!this.beforeDestroyLeave) this.reconnectWebSocket()
  console.log('WebSocket connection closed.')
}
  • 当 WebSocket 连接关闭时,调用 handleWebSocketClose。如果页面没有销毁,则尝试重新连接 WebSocket。
  • 控制台打印 WebSocket connection closed. 表示连接已关闭。
3. 心跳检测机制

心跳检测的作用是定期检查 WebSocket 连接是否正常。如果连接断开,自动重连。为了避免服务端超时断开连接,客户端定期发送 ping 消息到服务器,以保持连接的活跃。

javascript 复制代码
createHeartCheck() {
  return {
    timeout: 55000,
    timeoutObj: null,
    serverTimeoutObj: null,
    reset() {
      clearTimeout(this.timeoutObj)
      clearTimeout(this.serverTimeoutObj)
    },
    start() {
      this.reset()
      this.timeoutObj = setTimeout(() => {
        this.websocket.send('ping:websocket') // 向服务器发送心跳数据
        this.serverTimeoutObj = setTimeout(() => {
          this.websocket.close() // 如果超过指定时间没有响应,关闭 WebSocket 连接
        }, this.timeout)
      }, this.timeout)
    }
  }
}

心跳检测机制:

  • timeout 设置为 55000 毫秒(即 55 秒)。这是发送心跳的时间间隔。
  • timeoutObj 和 serverTimeoutObj 分别用于存储客户端和服务器的超时计时器。
  • reset() 方法用来清除现有的超时计时器。
  • start() 方法用来启动心跳检测:
    • 每隔 timeout 毫秒发送一条心跳消息 ping:websocket 给服务器。
    • 启动一个定时器,如果在 timeout 毫秒内没有收到响应,则认为连接断开,关闭 WebSocket。
4. WebSocket 重新连接机制

当 WebSocket 连接断开或出错时,尝试自动重连。防止重复连接导致多次 WebSocket 实例被创建。

javascript 复制代码
reconnectWebSocket() {
  if (this.lockReconnect) return // 如果正在重连,直接返回,避免重复连接
  this.lockReconnect = true
  setTimeout(() => {
    this.setupWebSocket() // 尝试重新建立 WebSocket 连接
    this.lockReconnect = false
  }, 5000) // 每隔 5 秒重连一次
}
  • 使用 lockReconnect 变量来防止多次重连请求,确保每次只会有一个重连请求在进行。
  • 重连间隔为 5 秒。
5. 滚动加载和历史数据
javascript 复制代码
updateHistoryData() {
  if (this.websocketData.length > 5) {
    this.historyList.push(this.websocketData[4]) // 将第五条数据推入历史记录
    this.websocketData.splice(5, 1) // 删除超过五条的数据
  }
  if (!this.isBindScrolled) {
    this.$nextTick(() => {
      const msg = document.querySelector('.list-box')
      msg.scrollTop = msg.scrollHeight // 滚动到底部
    })
  }
  this.restNums = this.isBindScrolled ? ++this.restNums : 0
  this.restComment = this.restNums >= 99 ? '99+' : this.restNums
}
  • 当 WebSocket 收到新消息时,调用 updateHistoryData() 更新消息展示。
  • 新消息插入到 this.websocketData 数组的前端,如果数据超过 5 条,则将第 5 条消息移动到历史记录
    this.historyList 中,并删除多余的消息。

总结

1. WebSocket 实现过程:

  • 在 setupWebSocket() 中通过 new WebSocket(url) 实例化WebSocket,设置连接、错误、消息、关闭等回调。
  • 使用心跳机制保持 WebSocket 连接的活跃,避免连接超时。
  • 当连接断开时,会自动进行重连,确保客户端始终保持连接状态。

2. WebSocket 心跳机制:

  • 通过定时发送 ping 消息来检测连接状态,如果超过设定时间没有收到响应,则关闭连接并进行重连。

3. 消息处理:

  • 每当接收到服务器推送的消息时,首先处理心跳消息,更新历史数据并控制滚动行为。

4.滚动加载机制:

  • 通过监听滚动条事件,动态加载更多历史记录。

这样,整个 WebSocket 的实现不仅保证了消息的实时推送,还通过心跳机制和重连机制增强了连接的稳定性。

代码示例

javascript 复制代码
<script>
import { mapState } from 'vuex'
import { getHistory } from '@/api/user'

export default {
  data() {
    return {
      time: '',
      week: '',
      date: '',
      deptId: '',
      websocket: null,
      heartCheck: null,
      lockReconnect: false,
      beforeDestroyLeave: false, // 是否离开页面
      websocketData: [],
      historyList: [],
      page: 1,
      pageSize: 10,
      dataLoading: false,
      finished: false,
      isBindScrolled: false,
      restNums: 0,
      restComment: 0,
      count: 0,
      winWidth: null,
      winHeight: null
    }
  },
  computed: {
    ...mapState('user', ['userInfo', 'vwSOaDeptInfo'])
  },
  created() {
    this.deptId = this.vwSOaDeptInfo.deptId
    this.heartCheck = this.createHeartCheck()
  },
  mounted() {
    this.setupWebSocket()
    this.setupWindowResizeListener()
    this.getCurrentTime()
  },
  beforeDestroy() {
    this.cleanupWebSocket()
  },
  filters: {
    recognitionType(v) {
      const type = {
        1: '员工',
        2: '访客',
        3: '重点人员',
        4: '陌生人',
        5: '未识别',
        6: 'VIP访客'
      }
      return type[v]
    }
  },
  methods: {
    setupWebSocket() {
      const env = process.env.VUE_APP_ENV
      const urls = {
        prod: 'wss://domain.name/net/websocket/',
        test: 'wss://domaintest.name/net/websocket/',
        dev: 'ws://127.0.0.0:8080/net/websocket/'
      }
      const wsUrl = urls[env] || urls.dev
      if ('WebSocket' in window) {
        this.websocket = new WebSocket(`${wsUrl}${this.deptId}`)
        this.websocket.onopen = this.handleWebSocketOpen
        this.websocket.onerror = this.handleWebSocketError
        this.websocket.onmessage = this.handleWebSocketMessage
        this.websocket.onclose = this.handleWebSocketClose
      } else {
        alert('WebSocket not supported')
        this.reconnectWebSocket()
      }
    },
    handleWebSocketOpen() {
      this.heartCheck.start()
      console.log('WebSocket connection established.')
    },
    handleWebSocketError(error) {
      if (!this.beforeDestroyLeave) this.reconnectWebSocket()
      console.error('WebSocket error:', error)
    },
    handleWebSocketMessage(event) {
      this.heartCheck.reset().start()
      const data = JSON.parse(event.data)
      if (data.code === '心跳检测成功') return
      this.websocketData.unshift(data)
      this.updateHistoryData()
    },
    handleWebSocketClose() {
      if (!this.beforeDestroyLeave) this.reconnectWebSocket()
      console.log('WebSocket connection closed.')
    },
    reconnectWebSocket() {
      if (this.lockReconnect) return
      this.lockReconnect = true
      setTimeout(() => {
        this.setupWebSocket()
        this.lockReconnect = false
      }, 5000)
    },
    createHeartCheck() {
      return {
        timeout: 55000,
        timeoutObj: null,
        serverTimeoutObj: null,
        reset() {
          clearTimeout(this.timeoutObj)
          clearTimeout(this.serverTimeoutObj)
        },
        start() {
          this.reset()
          this.timeoutObj = setTimeout(() => {
            this.websocket.send('ping:websocket')
            this.serverTimeoutObj = setTimeout(() => {
              this.websocket.close()
            }, this.timeout)
          }, this.timeout)
        }
      }
    },
    cleanupWebSocket() {
      this.beforeDestroyLeave = true
      this.heartCheck.reset()
      this.websocket.close()
    },
    setupWindowResizeListener() {
      window.addEventListener('resize', this.updateWindowDimensions)
      this.updateWindowDimensions()
    },
    updateWindowDimensions() {
      this.winWidth = window.innerWidth || document.documentElement.clientWidth
      this.winHeight = window.innerHeight || document.documentElement.clientHeight
    },
    getCurrentTime() {
      setInterval(this.updateTime, 1000)
    },
    updateTime() {
      const now = new Date()
      const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
      this.week = weekDays[now.getDay()]
      this.date = `${now.getMonth() + 1}月${now.getDate()}日 `
      this.time = `${now.getHours()}:${now.getMinutes().toString().padStart(2, '0')}`
    },
    queryHistory() {
      getHistory({ pageNumReq: this.page, pageSizeReq: this.pageSize, deptId: this.deptId })
        .then(res => {
          if (res.code === 0) {
            this.historyList = [...res.t.list.reverse(), ...this.historyList]
            this.page++
            this.dataLoading = false
            this.finished = this.historyList.length >= res.t.total || res.t.list.length === 0
            this.scrollToBottomIfNeeded()
          }
        })
        .catch(error => {
          this.dataLoading = false
          console.error('Error fetching history:', error)
        })
    },
    addData() {
      const newData = {
        createTime: '2022-03-09 14:05:32',
        personName: `${this.count}人`,
        recognitionType: 2,
        signType: '签到成功',
        visitType: 2
      }
      this.websocketData.unshift(newData)
      this.updateHistoryData()
    },
    updateHistoryData() {
      if (this.websocketData.length > 5) {
        this.historyList.push(this.websocketData[4])
        this.websocketData.splice(5, 1)
      }
      if (!this.isBindScrolled) {
        this.$nextTick(() => {
          const msg = document.querySelector('.list-box')
          msg.scrollTop = msg.scrollHeight
        })
      }
      this.restNums = this.isBindScrolled ? ++this.restNums : 0
      this.restComment = this.restNums >= 99 ? '99+' : this.restNums
    },
    scrool(e) {
      const ele = e.target
      if (!this.isBindScrolled && ele.scrollTop < 6) {
        this.isBindScrolled = true
        if (!this.finished) {
          this.queryHistory()
        }
      }
    },
    scrollToBottomIfNeeded() {
      if (!this.isBindScrolled) {
        this.$nextTick(() => {
          const msg = document.querySelector('.list-box')
          msg.scrollTop = msg.scrollHeight
        })
      }
    },
    back() {
      this.$router.replace('/home')
    }
  }
}
</script>
相关推荐
技术钱20 小时前
vue3基于 Vxe Table 实现可拖拽分组 + 动态求和的高级表格
javascript·vue.js
还是大剑师兰特20 小时前
Vue3 + Element Plus 日期选择器:开始 / 结束时间,结束时间不超过今天
前端·javascript·vue.js
不会写DN20 小时前
Js常用数组处理
开发语言·javascript·ecmascript
还是大剑师兰特20 小时前
数组中有两个数据,将其变成字符串
开发语言·javascript·vue.js
Saga Two20 小时前
Vue实现核心原理
前端·javascript·vue.js
技术钱20 小时前
vue3实现时间根据系统时区转换对应的时间
javascript·vue.js
殷忆枫20 小时前
基于STM32的ML307R连接Onenet平台
服务器·前端·javascript
Java 码农21 小时前
vue cli 环境搭建
前端·javascript·vue.js
酉鬼女又兒21 小时前
零基础入门前端JavaScript Object 对象完全指南:从基础到进阶(可用于备赛蓝桥杯Web应用开发赛道)
开发语言·前端·javascript·职场和发展·蓝桥杯
RPGMZ21 小时前
RPGMakerMZ游戏引擎 地图角色顶部显示称号
javascript·游戏引擎·rpgmz·rpgmakermz