什么是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()
}
}
}