WebSocket实现直播弹幕滚动推送效果
WebSocket + 弹幕滚动推送
- WebSocket 通信协议优点
- 实现过程详细解析
- 1. 初始化 WebSocket 连接
- 2. WebSocket 事件回调
- 2.2 连接错误 (onerror)
- 2.3 接收到消息 (onmessage)
- 2.4 连接关闭 (onclose)
- 3. 心跳检测机制
- 4. WebSocket 重新连接机制
- 5. 滚动加载和历史数据
- 总结
- 代码示例
WebSocket 通信协议优点
WebSocket 是一种通信协议,它提供了一个全双工、低延迟的通信通道,允许客户端和服务器之间进行实时的数据交换。它比传统的轮询请求方式更加高效,因为它建立在持久连接之上,而不需要每次都重新建立连接。使用 WebSocket,可以实现实时数据推送、在线聊天、消息通知等功能。
双向通信: WebSocket 允许客户端和服务器之间进行双向通信,这意味着双方都可以主动发送消息,而不仅仅是客户端向服务器发送请求。
降低延迟: 相较于传统的 HTTP 协议,WebSocket 在连接建立后能保持一个持久连接,从而减少了每次请求和响应之间的延迟。
节省带宽: WebSocket 使用一种轻量级的帧格式,相比于 HTTP,相同的数据传输量能够消耗更少的带宽和资源。
实时性: WebSocket 特别适合需要实时数据更新的应用,比如在线游戏、即时聊天应用、实时股票更新或体育赛事直播。
简单的 API: WebSocket 提供了易用的 JavaScript API,使得开发人员能够轻松实现复杂的实时通信功能。
适用场景:
- 在线游戏:需要快速、安全、实时的数据传输。
- 聊天应用:实现实时的消息推送。
- 股票市场:实时更新数据的应用程序。
- 协作工具:如在线文档编辑,允许多用户实时便捷地进行协作。
实现过程详细解析
详细解析在 Vue 代码中 WebSocket实现直播弹幕滚动效果过程,逐步说明各个方法和关键部分。
1. 初始化 WebSocket 连接
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)
handleWebSocketOpen() {
this.heartCheck.start()
console.log('WebSocket connection established.')
}
- 当 WebSocket 连接成功时,调用 handleWebSocketOpen。在这里,调用了 heartCheck.start()
来启动心跳检测,保持连接的持续性。 - 控制台打印 WebSocket connection established. 来表明连接成功。
2.2 连接错误 (onerror)
handleWebSocketError(error) {
if (!this.beforeDestroyLeave) this.reconnectWebSocket()
console.error('WebSocket error:', error)
}
- 当 WebSocket 连接发生错误时,调用 handleWebSocketError。此时,尝试重新连接 WebSocket (this.reconnectWebSocket())。并在控制台输出错误信息。
2.3 接收到消息 (onmessage)
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)
handleWebSocketClose() {
if (!this.beforeDestroyLeave) this.reconnectWebSocket()
console.log('WebSocket connection closed.')
}
- 当 WebSocket 连接关闭时,调用 handleWebSocketClose。如果页面没有销毁,则尝试重新连接 WebSocket。
- 控制台打印 WebSocket connection closed. 表示连接已关闭。
3. 心跳检测机制
心跳检测的作用是定期检查 WebSocket 连接是否正常。如果连接断开,自动重连。为了避免服务端超时断开连接,客户端定期发送 ping 消息到服务器,以保持连接的活跃。
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 实例被创建。
reconnectWebSocket() {
if (this.lockReconnect) return // 如果正在重连,直接返回,避免重复连接
this.lockReconnect = true
setTimeout(() => {
this.setupWebSocket() // 尝试重新建立 WebSocket 连接
this.lockReconnect = false
}, 5000) // 每隔 5 秒重连一次
}
- 使用 lockReconnect 变量来防止多次重连请求,确保每次只会有一个重连请求在进行。
- 重连间隔为 5 秒。
5. 滚动加载和历史数据
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 的实现不仅保证了消息的实时推送,还通过心跳机制和重连机制增强了连接的稳定性。
代码示例
<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>