首先介绍下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 及其以上版本。 服务器长期维护长连接需要一定的成本,各个浏览器支持程度不一; 【需要后端代码稳定,受网络限制大,兼容性差,维护成本高,生态圈小】
使用场景
-
协同编辑:
- 场景描述:WebSocket可用于实现多人协同编辑,如在线文档协作、团队代码编辑等。
- 实际应用:多个用户可以同时编辑同一个文档或代码文件,他们的编辑结果会实时地同步到其他用户的界面上。
.
-
实时监控:
- 场景描述:WebSocket适用于实时监控系统,如监控设备的运行状态、实时监测交通流量等。
- 实际应用:服务器可以实时地将监控数据推送给客户端,客户端可以及时地显示最新的监控信息。
.
-
实时聊天:
- 场景描述:WebSocket是即时通讯的理想选择,如在线聊天室、多人游戏等。
- 实际应用:客户端和服务器可以实时地发送和接收消息,无需频繁地发起HTTP请求。
.
-
实时数据更新:
- 场景描述:WebSocket能够实时推送数据更新,如实时股票行情、实时天气预报等。
- 实际应用:服务器可以实时地将最新的数据推送给客户端,客户端无需主动发起请求。
.
-
游戏开发:
- 场景描述: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 客户端库或框架都会提供断网重连的功能。
-
连接状态监听:客户端需要监听 WebSocket 的 onclose 事件,该事件会在连接关闭时被触发。一旦接收到 onclose 事件,客户端就知道连接已经断开。
-
重连策略:在连接断开后,客户端会根据预定义的重连策略来决定是否尝试重新连接。重连策略可以包括重连间隔、重连次数限制等。例如,客户端可能会等待一段时间后再次尝试连接,如果连接仍然失败,它会继续等待更长的时间再次尝试,直到达到最大重连次数或用户干预。
-
重连实现:在决定重连后,客户端会重新调用 WebSocket 的构造函数或相关方法,以尝试与服务器建立新的 WebSocket 连接。如果连接成功,客户端会恢复正常的通信。
-
心跳检测:为了更准确地检测连接状态,很多 WebSocket 客户端还会实现心跳检测机制。客户端定期向服务器发送心跳消息,如果服务器在规定的时间内没有响应,客户端就认为连接已经断开,并开始执行重连逻辑。
-
日志和通知:在重连过程中,客户端可能会记录日志以便后续分析,并在需要时向用户或开发者发送通知。
心跳检测
WebSocket 心跳(Heartbeat)是指为了在 WebSocket 连接中检测连接的活跃性和可用性而定期发送的简短消息。WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,允许服务器主动向客户端推送信息,客户端也可以主动向服务器发送信息。
由于网络环境的复杂性,如网络不稳定、防火墙或代理服务器配置不当等原因,WebSocket 连接有时可能会“假死”,即连接仍然处于打开状态(readyState 为 OPEN),但实际上数据已经无法传输。在这种情况下,如果没有任何数据在连接上传输,双方都无法得知连接是否已经失效。
为了解决这个问题,可以定期发送心跳消息来检测连接的活跃性。心跳消息通常是一个简短的数据包,例如一个简单的字符串或数字。客户端和服务器都可以发送心跳消息,但通常是由客户端发送,因为服务器可以更容易地检测和管理多个连接。
当接收到心跳消息时,接收方会重置其心跳定时器,并知道连接仍然是活跃的。如果在指定的时间间隔内没有收到心跳消息,接收方可以认为连接已经失效,并采取相应的措施,如关闭连接、尝试重新连接或通知用户。
-
发送频率:心跳消息的发送频率应该根据具体的应用场景和网络环境来确定。过于频繁的心跳消息可能会增加网络负担,而发送间隔过长则可能无法及时发现连接问题。
-
消息内容:心跳消息的内容应该简短,以减少网络带宽的占用。通常,一个简单的字符串或数字就足够了。
-
处理接收到的消息:当接收到心跳消息时,接收方应该重置其心跳定时器,并继续等待下一个心跳消息。如果接收到非心跳消息,则应该根据应用的逻辑来处理。
-
重连机制:如果心跳检测发现连接已经失效,接收方应该尝试重新建立连接。重连的间隔和次数可以根据需要来配置。
主要封装如下:封装成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在旧浏览器或支持有限的环境中的兼容性。在这种情况下,可以使用回退机制。
以上就是整体的封装及使用,对你有用的话,点赞收藏起来吧!