webSocket实战心跳机制、断线重连

1,632 阅读5分钟

感谢DieHunter1024WebSocket使用及优化(心跳机制与断线重连)

为了自用方便,记录一下

一、websocket demo

实现websocket基本功能

服务端

服务端采用node+ws模块搭建websocket服务
在server文件夹初始化npm,下载ws模块

npm init -y 
npm i ws

新建server.js

const WebSocket = require('ws');
const port = 1024//端口
const pathname = '/ws/'//访问路径

new WebSocket.Server({port}, function () {
    console.log('websocket服务开启')
}).on('connection', connectHandler)

function connectHandler (ws) {
    console.log('客户端连接')
    ws.on('error', errorHandler)
    ws.on('close', closeHandler)
    ws.on('message', messageHandler)
}

function messageHandler (e) {
    console.info('接收客户端消息',e.toString())
    let temp = e.toString()
    this.send(temp)
}

function errorHandler (e) {
    console.info('客户端出错')
}

function closeHandler (e) {
    console.info('客户端已断开')
}

前端

websocket.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script type="module">
    const name = 'test'// 连接用户名
    let wsUrl = 'ws://127.0.0.1:1024/ws/?name=' + name
    
    // 连接上时回调
    const ws = new WebSocket(wsUrl)
    ws.onopen = function (e) {
        console.log('开启')
        ws.send(JSON.stringify({
            ModeCode: 'message',
            msg: 'hello',
        }))
    }

    // 断开连接时回调
    ws.onclose = function (e) {
        console.log('关闭')
    }
    
    // 收到服务端消息
    ws.onmessage = function (event) {
      let data = JSON.parse(event.data)
      console.log('收到消息' + data.msg)
      ws.close()
    }
    
    // 连接出错
    ws.onerror = function (e) {
        console.log('出错')
    }
  </script>
</body>
</html>

前端打印结果:
image.png 服务端打印结果:
image.png

二、优化

优化服务端

在服务端,先把server完善一下,通过http的upgrade过滤验证ws连接
在原有的服务端增加http服务并做好路径验证
完成httpServer后,再完善一下websocket服务,将每一个连接的用户都通过代理保存并实现增删,得到完整的服务端
server.js

const http = require('http');
const WebSocket = require('ws');
const port = 1024; // 端口
const pathname = '/ws/'; // 访问路径
const server = http.createServer();

class WebSocketServer extends WebSocket.Server {
    constructor(){
        super(...arguments);
        this.webSocketClient = {}; // 存放已连接的客户端
    }

    set ws(val){ // 代理当前的ws,赋值时将其初始化
        this._ws = val
        val.t = this;
        val.on('error', this.errorHandler)
        val.on('close', this.closeHandler)
        val.on('message', this.messageHandler)
    }

    get ws(){
        return this._ws
    }

    messageHandler(e){
        console.info('接收客户端消息')
        let data = JSON.parse(e.toString())
        switch(data.ModeCode) {
            case 'message':
                console.log('收到消息' + data.msg)
                this.send(e.toString())
                break;
            case 'heart_beat':
                console.log(`收到${this.name}心跳${data.msg}`)
                this.send(e.toString())
                break;
        }
    }

    errorHandler(e){
        this.t.removeClient(this)
        console.info('客户端出错')
    }

    closeHandler(e){
        this.t.removeClient(this)
        console.info('客户端已断开')
    }

    // 设备上线时添加到客户端列表
    addClient(item){
        if(this.webSocketClient[item['name']]) {
            console.log(item['name'] + '客户端已存在')
            this.webSocketClient[item['name']].close()
        }
        console.log(item['name'] + '客户端已添加')
        this.webSocketClient[item['name']] = item
    }

    // 设备断线时从客户端列表删除
    removeClient(item){
        if(!this.webSocketClient[item['name']]) {
            console.log(item['name'] + '客户端不存在')
            return;
        }
        console.log(item['name'] + '客户端已移除')
        this.webSocketClient[item['name']] = null
    }
}

const webSocketServer = new WebSocketServer({noServer: true})
server.on("upgrade", (req, socket, head) => { // 通过http.server过滤数据
    let url = new URL(req.url, `http://${req.headers.host}`)
    let name = url.searchParams.get('name') // 获取连接标识
    if(!checkUrl(url.pathname, pathname)) { // 未按标准
        socket.write('未按照标准访问');
        socket.destroy();
        return;
    }
    webSocketServer.handleUpgrade(req, socket, head, function (ws) {
        ws.name = name; // 添加索引,方便在客户端列表查询某个socket连接
        webSocketServer.addClient(ws);
        webSocketServer.ws = ws
    });
})
server.listen(port, () => {
    console.log('服务开启')
})

// 验证url标准
function checkUrl(url, key){ // 判断url是否包含key
    return - ~ url.indexOf(key)
}

优化前端,添加简单的控制功能

把客户端的websocket完善优化一下,添加一些简单的控制功能(连接,发消息,断开)的按钮,这里需要注意:在下次连接之前一定要先关闭当前连接,否则会导致多个客户端同时连接,消耗性能

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>webSocket(有控制按钮)</title>
</head>
<body>
    <button id="connect">连接</button>
    <button disabled id="sendMessage">发送</button>
    <button disabled id="destroy">关闭</button>
    <script type="module">
        const name = 'test' //连接用户名
        let connect = document.querySelector('#connect'), //连接按钮
            sendMessage = document.querySelector('#sendMessage'), //发送按钮
            destroy = document.querySelector('#destroy'), //关闭按钮
            wsUrl = 'ws://127.0.0.1:1024/ws/?name=' + name, //连接地址
            ws;

        connect.addEventListener('click', connectWebSocket)
        sendMessage.addEventListener('click', function (e) {
            ws.send(JSON.stringify({
                ModeCode: "message",
                msg: 'hello'
            }))
        })
        destroy.addEventListener('click', function (e) {
            ws.close()
            ws = null
        })

        function connectWebSocket () {
            if(!ws) { //第一次执行,初始化或ws断开时可执行
                ws = new WebSocket(wsUrl)
                initWebSocket()
            }
        }

        function initWebSocket () {
            //连接上时回调
            ws.onopen = function (e) {
                setButtonState('open')
                console.log('开启')
            }
            //断开连接时回调
            ws.onclose = function (e) {
                setButtonState('close')
                console.log('关闭')
            }
            //收到服务端消息
            ws.onmessage = function (e) {
                let data = JSON.parse(e.data)
                console.log('收到服务端消息:' + data.msg)
            }
            //连接出错
            ws.onerror = function (e) {
                setButtonState('close')
                console.log('出错')
            }
        }

        /*
        * 设置按钮是否可点击
        * @param state:open表示开启状态,close表示关闭状态
        */
        function setButtonState (state) {
            switch(state) {
                case 'open':
                    connect.disabled = true
                    sendMessage.disabled = false
                    destroy.disabled = false
                    break;
                case 'close':
                    connect.disabled = false
                    sendMessage.disabled = true
                    destroy.disabled = true
                    break;
            }
        }
    </script>
</body>
</html>

优化前端,添加心跳机制、断线重连

websocket心跳机制:客户端每隔一段时间向服务端发送一个特有的心跳消息,每次服务端收到消息后只需将消息返回,此时,若二者还保持连接,则客户端就会收到消息,若没收到,则说明连接断开,此时,客户端就要主动重连,完成一个周期
心跳的实现也很简单,只需在第一次连接时用回调函数做延时处理,此时还需要设置一个心跳超时时间,若某时间段内客户端发送了消息,而服务端未返回,则认定为断线。下面,我就来实现一下心跳

//this.heartBeat  --->  time:心跳时间间隔 timeout:心跳超时间隔
/*
 * 心跳初始函数
 * @param time:心跳时间间隔
 */
function startHeartBeat (time) {
    setTimeout(() => {
        this.sendMsg({
            ModeCode: ModeCode.HEART_BEAT,
            msg: new Date()
        })
        this.waitingServer()
    }, time)
}
//延时等待服务端响应,通过webSocketState判断是否连线成功
function waitingServer () {
    this.webSocketState = false//在线状态
    setTimeout(() => {
        if(this.webSocketState) {
            this.startHeartBeat(this.heartBeat.time)
            return
        }
        console.log('心跳无响应,已断线')
        this.close()
        //重连操作
    }, this.heartBeat.timeout)
}

三、最终版websocket实战(含心跳机制、断线重连)

服务端

同前面的server.js

前端

websocket.html

<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>webSocket(含心跳机制、断线重连)</title>
</head>
<body>
    <button id="connect">连接</button>
    <button disabled id="sendMessage">发送</button>
    <button disabled id="destroy">关闭</button>
    <script type="module">
        import MyWebSocket from './js/webSocket.js'
        import eventBus from './js/eventBus.js'

        const name = 'test' //连接用户名
        let connect = document.querySelector('#connect')
        let sendMessage = document.querySelector('#sendMessage')
        let destroy = document.querySelector('#destroy')
        let myWebSocket,
            wsUrl = 'ws://127.0.0.1:1024/ws/?name=' + name

        eventBus.onEvent('changeBtnState', setButtonState) //设置按钮样式
        eventBus.onEvent('reconnect', reconnectWebSocket) //接收重连消息
        connect.addEventListener('click', reconnectWebSocket)
        sendMessage.addEventListener('click', function (e) {
            myWebSocket.sendMsg({
                ModeCode: "message",
                msg: 'hello'
            })
        })
        destroy.addEventListener('click', function (e) {
            myWebSocket.close()
        })

        function reconnectWebSocket () {
            if(!myWebSocket) {//第一次执行,初始化
                connectWebSocket()
            }
            if(myWebSocket && myWebSocket.reconnectTimer) {//防止多个websocket同时执行
                clearTimeout(myWebSocket.reconnectTimer)
                myWebSocket.reconnectTimer = null
                connectWebSocket()
            } 
        } 
    
        function connectWebSocket () {
            myWebSocket = new MyWebSocket(wsUrl);
            myWebSocket.init({//time:心跳时间间隔 timeout:心跳超时间隔 reconnect:断线重连时
                time: 3 * 1000,
                timeout: 3 * 1000,
                reconnect: 5 * 1000
            }, true)
        }

        /*
        * 设置按钮是否可点击
        * @param state:open表示开启状态,close表示关闭状态
        */
        function setButtonState (state) {
            switch(state) {
                case 'open':
                    connect.disabled = true
                    sendMessage.disabled = false
                    destroy.disabled = false
                    break;
                case 'close':
                    connect.disabled = false
                    sendMessage.disabled = true
                    destroy.disabled = true
                    break;
            }
        }
    </script>
</body>
</html>

webSocket.js

import eventBus from "../js/eventBus.js"

const ModeCode = {//websocket消息类型
    MSG: 'message',//普通消息
    HEART_BEAT: 'heart_beat'//心跳
}

export default class MyWebSocket extends WebSocket {
    constructor (url, protocols) {
        super(url, protocols);
        return this
    }

    /*
     * 入口函数
     * @param heartBeatConfig  time:心跳时间间隔 timeout:心跳超时间隔 reconnect:断线重连时间间隔
     * @param isReconnect 是否断线重连
     */
    init (heartBeatConfig, isReconnect) {
        this.onopen = this.openHandler//连接上时回调
        this.onclose = this.closeHandler//断开连接时回调
        this.onmessage = this.messageHandler//收到服务端消息
        this.onerror = this.errorHandler//连接出错
        this.heartBeat = heartBeatConfig
        this.isReconnect = isReconnect
        this.reconnectTimer = null//断线重连时间器
        this.webSocketState = false//socket状态 true为已连接
    }

    openHandler () {
        eventBus.emitEvent('changeBtnState', 'open')//触发事件改变按钮样式
        this.webSocketState = true//socket状态设置为连接,做为后面的断线重连的拦截器
        this.heartBeat && this.heartBeat.time ? this.startHeartBeat(this.heartBeat.time) : ""//是否启动心跳机制
        console.log('开启')
    }

    messageHandler (e) {
        let data = this.getMsg(e)
        switch(data.ModeCode) {
            case ModeCode.MSG://普通消息
                console.log('收到消息' + data.msg)
                break;
            case ModeCode.HEART_BEAT://心跳
                this.webSocketState = true
                console.log('收到心跳响应' + data.msg)
                break;
        }
    }

    //socket关闭
    closeHandler () {
        eventBus.emitEvent('changeBtnState', 'close')//触发事件改变按钮样式
        this.webSocketState = false//socket状态设置为断线
        console.log('关闭')
    }

    //socket出错
    errorHandler () {
        eventBus.emitEvent('changeBtnState', 'close')//触发事件改变按钮样式
        this.webSocketState = false//socket状态设置为断线
        this.reconnectWebSocket()//重连
        console.log('出错')
    }

    sendMsg (obj) {
        this.send(JSON.stringify(obj))
    }

    getMsg (e) {
        return JSON.parse(e.data)
    }

    /*
     * 心跳初始函数
     * @param time:心跳时间间隔
     */
    startHeartBeat (time) {
        setTimeout(() => {
            this.sendMsg({
                ModeCode: ModeCode.HEART_BEAT,
                msg: new Date()
            })
            this.waitingServer()
        }, time)
    }

    //延时等待服务端响应,通过webSocketState判断是否连线成功
    waitingServer () {
        this.webSocketState = false
        setTimeout(() => {
            if(this.webSocketState) {
                this.startHeartBeat(this.heartBeat.time)
                return
            }
            console.log('心跳无响应,已断线')
            try {
                this.onclose()
            } catch(e) {
                console.log('连接已关闭,无需关闭')
            }
            this.reconnectWebSocket()
        }, this.heartBeat.timeout)
    }

    //重连操作
    reconnectWebSocket () {
        if(!this.isReconnect) {
            return;
        }
        this.reconnectTimer = setTimeout(() => {
            eventBus.emitEvent('reconnect')
        }, this.heartBeat.reconnect)
    }
}

eventBus.js

// 发布/订阅设计模式(Pub/Sub)
class EventBus {
  constructor() {
      this._eventList = {} //调度中心列表
  }

  static Instance() { //返回当前类的实例的单例
      if (!EventBus._instance) {
          Object.defineProperty(EventBus, "_instance", {
              value: new EventBus()
          });
      }
      return EventBus._instance;
  }
  /**
   * 注册事件至调度中心
   * @param type 事件类型,特指具体事件名
   * @param fn 事件注册的回调
   */
  onEvent(type, fn) { //订阅者
      if (!this.isKeyInObj(this._eventList, type)) { //若调度中心未找到该事件的队列,则新建某个事件列表(可以对某个类型的事件注册多个回调函数)
          Object.defineProperty(this._eventList, type, {
              value: [],
              writable: true,
              enumerable: true,
              configurable: true
          })
      }
      this._eventList[type].push(fn)
  }
  /**
   * 触发调度中心的某个或者某些该事件类型下注册的函数
   * @param type 事件类型,特指具体事件名
   * @param data 发布者传递的参数
   */
  emitEvent(type, data) { //发布者
      if (this.isKeyInObj(this._eventList, type)) {
          for (let i = 0; i < this._eventList[type].length; i++) {
              this._eventList[type][i] && this._eventList[type][i](data)
          }
      }
  }
  offEvent(type, fn) { //销毁监听
      for (let i = 0; i < this._eventList[type].length; i++) {
          if (this._eventList[type][i] && this._eventList[type][i] === fn) {
              this._eventList[type][i] = null
          }
      }
  }
  /**
   * 检查对象是否包含该属性,除原型链
   * @param obj 被检查对象
   * @param key 被检查对象的属性
   */
  isKeyInObj(obj, key) {
      if (Object.hasOwnProperty.call(obj, key)) {
          return true
      }
      return false
  }
}

export default EventBus.Instance()