目录
[第一章 前言](#第一章 前言)
[第二章 实现流程](#第二章 实现流程)
[2.1 场景](#2.1 场景)
[2.2 流程概览](#2.2 流程概览)
[2.3 具体实现(前端)](#2.3 具体实现(前端))
[2.3.1 连接ws](#2.3.1 连接ws)
[2.3.2 心跳+重连](#2.3.2 心跳+重连)
[2.3.3 流程扭转](#2.3.3 流程扭转)
[2.3.4 遇到的问题](#2.3.4 遇到的问题)
第一章 前言
前阵子需求排山倒海,日报里全是"WebSocket 断连""SSE 又 502"的 flag,小编一边改 bug 一边掉头发,根本腾不出手写干货。现在!终于!把 WebSocket 和 SSE 双双撸到线上后,小编带着新鲜热乎的踩坑笔记回来了!
原理小编就不做过多介绍了,从短轮询 -> 长轮询 -> sse -> websocket的整个流程、原理,大家如果感兴趣可以到小编的下面这篇文章看一看!
第二章 实现流程
2.1 场景
线上项目竞拍,可能多人同时出价,5 秒内必须把最新价推给所有人,断网 3 秒内需自动重连,连上后恢复数据。区分不同的角色下有不同的操作。
2.2 流程概览
- 基础流程:
- 具体流程
2.3 具体实现(前端)
2.3.1 连接ws
当用户进入页面时,连接wbsocket,与后端建立通信:
- ws方法:
javascript
/**
* WebSocket连接管理工具
* 用于处理竞价和报名功能的WebSocket连接
*/
/** 竞价WebSocket连接实例 */
let bidWs = null
/** 最大重连次数 */
const MAX_RECONNECT_NUM = 5
/** 当前重连次数 */
let RECONNECT_NUM = 0
/**
* 启动竞价WebSocket连接
* @param {Function} callBack - 接收到消息时的回调函数
* @param {string} url - WebSocket连接地址
*/
export function startBidSockets(callBack, url) {
console.log('竞价WebSocket连接地址:', url)
// 浏览器兼容性处理
if ('WebSocket' in window) {
bidWs = new WebSocket(url)
} else if ('MozWebSocket' in window) {
// eslint-disable-next-line no-undef
bidWs = new MozWebSocket(url)
} else {
alert(
'浏览器版本过低,请升级您的浏览器。\r\n浏览器要求:Chrome14+/FireFox7+/Opera11+'
)
return
}
// 连接成功回调
bidWs.onopen = function () {
console.log('竞价WebSocket连接成功')
RECONNECT_NUM = 0 // 重置重连次数
// 监听连接关闭事件,实现自动重连
bidWs.addEventListener('close', () => {
console.log('WebSocket 断开连接============================')
if (RECONNECT_NUM < MAX_RECONNECT_NUM) {
setTimeout(() => {
RECONNECT_NUM++
console.log(`正在尝试第${RECONNECT_NUM}次重连...`)
startBidSockets(callBack, url)
}, 3000)
} else {
console.warn("已达到最大重试次数,重连失败!")
}
})
}
// 接收消息回调
bidWs.onmessage = function (result) {
console.log('竞价WebSocket返回的信息:', result)
callBack(result.data)
}
// 关闭时发送停止消息(已注释)
// bidWs.onclose = function () {
// bidWs.send('stop')
// }
}
/**
* 发送竞价WebSocket消息
* @param {string|Object} data - 要发送的数据
*/
export function sendBidWs(data) {
bidWs.send(data)
}
/**
* 检查竞价WebSocket连接状态
* @returns {boolean} - 连接是否正常
*/
export function checkBidWs() {
if (bidWs && bidWs.readyState === 1) { // readyState 1 表示连接已建立
return true
} else {
return false
}
}
/**
* 关闭竞价WebSocket连接
*/
export function closeBidWs() {
console.log('关闭竞价WebSocket连接')
bidWs.close()
}
/** 报名WebSocket连接实例(这里是进入竞价间的ws) */
let signWs = null
/**
* 启动报名WebSocket连接
* @param {Function} callBack - 接收到消息时的回调函数
* @param {string} url - WebSocket连接地址
* @param {Object} userInfo - 用户信息对象
*/
export function startSignSockets(callBack, url, userInfo) {
console.log('报名WebSocket连接地址:', url)
// 浏览器兼容性处理
if ('WebSocket' in window) {
signWs = new WebSocket(url)
} else if ('MozWebSocket' in window) {
// eslint-disable-next-line no-undef
signWs = new MozWebSocket(url)
} else {
alert(
'浏览器版本过低,请升级您的浏览器。\r\n浏览器要求:Chrome14+/FireFox7+/Opera11+'
)
return
}
// 连接成功回调
signWs.onopen = function () {
// 发送用户信息
console.log('刚进来发送用户信息:', JSON.stringify(userInfo))
signWs.send(JSON.stringify(userInfo))
console.log('报名WebSocket连接成功')
signWs.send('stop')
}
// 接收消息回调
signWs.onmessage = function (result) {
callBack(result.data)
}
// 连接关闭回调
signWs.onclose = function () {
signWs.send('stop')
}
}
/**
* 检查报名WebSocket连接状态
* @returns {boolean} - 连接是否正常
*/
export function checkSignWs() {
if (signWs && signWs.readyState === 1) { // readyState 1 表示连接已建立
return true
} else {
return false
}
}
/**
* 发送报名WebSocket消息
* @param {string|Object} data - 要发送的数据
*/
export function sendSignWs(data) {
signWs.send(data)
}
/**
* 关闭报名WebSocket连接
*/
export function closeSignWs() {
console.log('关闭报名WebSocket连接')
signWs.close()
}
- 使用:
javascript
import {
startBidSockets,
startSignSockets,
closeSignWs,
closeBidWs,
} from '@/utils/socketWxzjcs'
// 初始化连接
initWebsocket() {
const signUrl =
config.wxzjcsWebSocketUrl + `***/${this.query.id}`
const bidUrl = config.wxzjcsWebSocketUrl + `***/${this.query.id}`
startSignSockets(this.getInstitutionData, signUrl, {
content: userInfo,
})
startBidSockets(this.getBidPriceData, bidUrl)
},
2.3.2 心跳+重连
小编上面给出的需求是只要求重连(因为后端原因,不想加心跳);接下来小编会把心跳跟重连的步骤一起放在下面:
javascript
// heartbeat.js
const HEARTBEAT_INTERVAL = 12000; // 服务端超时
const PONG_TIMEOUT = 3000; // 前端超时
export function startHeartbeat(ws) {
const timer = setInterval(() => {
if (!ws.isAlive) return ws.terminate();
ws.isAlive = false;
ws.ping(); // 原生 ping 帧,浏览器会回 pong
}, HEARTBEAT_INTERVAL);
ws.on('pong', () => { ws.isAlive = true; });
ws.on('close', () => clearInterval(timer));
}
// 前端(Vue3)
let pongTimeout = null;
function sendPing() {
if (ws.readyState !== 1) return;
ws.send(JSON.stringify({ type: 'ping' }));
pongTimeout = setTimeout(() => {
ws.close(); // 触发 onclose → 重连
}, PONG_TIMEOUT);
}
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'pong') {
clearTimeout(pongTimeout);
}
};
// 放在ws连接成功后面
setInterval(sendPing, 5000);
- 重连策略:
- 第一次 1 秒,最大 30 秒,退避公式 min(2^try * 1000, 30000)
- 重连成功后,服务端把缺失历史出价数据再给前端,前端针对这些数据再做处理,实现"断网续传"
2.3.3 流程扭转
走通上面需求之后 小编还剩流程扭转:比如签到倒计时结束之后到竞价,竞价过程又有多长时间,这期间又会出现如果最后几分钟有人出价则进入延时阶段,竞价结束后到候选阶段,候选结束后才出结果......最后还有可能用户在某个阶段的过程中图退出竞价间,再进来的时候要把竞价间数据、流程都实时跟上!
- 解决方案:
- 小编的处理是前端根据倒计时、以及竞价方式的逻辑处理来决定到了某个阶段的,每个阶段的最终事件是通过后端接口拿的,利用后端给的阶段终止时间 - 当前时间 得到的就是 整个流程还剩多长时间;进入了那个阶段也是通过具体逻辑实现;(这样做的一个缺点就是用户中途退出时候再进入没法知道具体到了哪个流程)
- 1中缺点解决:然后端再ws返回的数据中返回所有的历史数据,通过历史数据中的标记,小编再确定是到了哪个流程,从而再衔接1中的倒计时
2.3.4 遇到的问题
- 倒计时如何准,以及各个用户之间倒计时是统一的,倒计时n秒n秒跳!
注意:一定不要使用new Date(),这个是获取的电脑本地的时间!不同电脑本地时间有差异,甚至可以改!
解决方案:后端接口获取当前时间,如果每次流程扭转了,也要获取后端给的时间再处理倒计时;以及用户挂后台后再进来,退出重进等等......还有最重要的一点,每次退出记得注销计时器!!
- 页面挂在后台时断开ws链接以及流程,进入时又重新连上:
- 自动播报问题:
浏览器安全机制:视频播放权限问题
关于navigator.mediaDevices为undefined,获取不到媒体权限的问题
-
心跳过密
前端 1 秒一次 ping 会占 5 % 带宽,合理 5 s 一次足够。