websocket重连机制

1,504 阅读7分钟
https://juejin.cn/post/6844904007593361415?searchId=202401100923491BA6C3FAA62F75EA1E17
https://www.jianshu.com/p/e4532bd0da57

前端作为主动方,定时给后端发送ping消息,解决1.网络问题引起的websocket断开(如果设备网络断开,不会触发任何函数)2.后端websocket服务也可能出现异常,连接断开后但是前端也并没有收到通知,(不会收到通知)

为什么需要心跳机制?

在使用原生 Websocket 的时候,如果设备网络断开,不会立刻触发 Websocket 的任何事件,前端也就无法得知当前连接是否已经断开。这个时候如果调用 Websocket.send 方法,浏览器才会发现链接断开了,便会立刻或者一定时间后(不同浏览器或者浏览器版本可能表现不同)触发 onclose 函数。

后端 Websocket 服务也可能出现异常,造成连接断开,这时前端也并没有收到断开通知,因此需要前端定时发送心跳消息 ping,后端收到 ping 类型的消息,立马返回消息,告知前端连接正常。如果一定时间没收到消息,就说明连接不正常,前端便会执行重连。

所以现在就有以下两个问题

    1. 如果前端没有发送消息,如果网络断开了,前端是不知道的,没法收到后端发过来的消息
    1. 网络断开后,后端websocket服务出现异常,但是没办法通知到前端

为了解决以上两个问题,以前端作为主动方,定时发送 ping 消息,用于检测网络和前后端连接问题。一旦发现异常,前端持续执行重连逻辑,直到重连成功。

// WebSocket构造函数,创建WebSocket对象
let ws = new WebSocket('ws://localhost:8888')

// 连接成功后的回调函数
ws.onopen = function (params) {
  console.log('客户端连接成功')
  // 向服务器发送消息
  ws.send('hello')
};

// 从服务器接受到信息时的回调函数
ws.onmessage = function (e) {
  console.log('收到服务器响应', e.data)
};

// 连接关闭后的回调函数
ws.onclose = function(evt) {
  console.log("关闭客户端连接");
};

// 连接失败后的回调函数
ws.onerror = function (evt) {
  console.log("连接失败了");
};


// 监听窗口关闭事件(关闭刷新),当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,这样服务端会抛异常。
类似方法:https://blog.csdn.net/weixin_44521565/article/details/110135974
// window.onbeforeunload = function() {
    //     ws.close();
    // }
window.onbeforeunload = function() {
        if (ws && ws.readyState === WebSocket.OPEN) {
            ws.close()
        }
        this.websocket = null; // 将WebSocket对象设置为null
    }


主要看这段代码:


<template>
  <div class="box">
    websocket
  </div>
</template>
<script>
    const ModeCode = {
        // websocket消息类型
        MSG: 'message', // 普通消息
        HEART_BEAT: 'heart_beat' // 心跳
    };
    const heartCheck = {
        timeout: 30 * 1000, //执行心跳的时间间隔
        timer: null, //  执行心跳的定时器
        serverTimeout: 60 * 1000, //心跳返回数据的超时时间--------超时未返回数据,说明后端断开了 触发close实现reconnect重连
        serverTimer: null, //服务器超时定时器
        reset() {
            this.timer && clearTimeout(this.timer)
            this.serverTimer && clearTimeout(this.serverTimer)
        },
        start(ws) {
            this.reset()
            this.timer = setTimeout(() => {
                // console.log('发送心跳,后端收到后,返回一个心跳消息')
                // onmessage拿到返回的心跳就说明连接正常
                ws.send(
                        JSON.stringify({
                            ModeCode: ModeCode.HEART_BEAT, //心跳类型
                            msg: new Date()
                        })
                    )
                    // 首先上面发送了心跳,这里在等待返回 如果有返回 则会在onmessage里重新调用start发送心跳,会重置定时器,超时(serverTimeout)没返回数据 就会close调用重连
                this.serverTimer = setTimeout(() => {
                    // 如果超过一定时间还没响应(响应后触发重置),说明后端断开了
                    ws.close()
                }, this.serverTimeout)
            }, this.timeout)
        }
    }
    export default {
        name: 'Websocket',
        data() {
            return {
                asrc,
                wsuri: 'ws://123.207.167.163:9010/ajaxchattest', // ws wss
                lockReconnect: false, // //避免重复连接
                maxReconnect: 5, // 最大重连次数,若连接失败
                socket: null
            }
        },
        mounted() {
            this.initWebSocket()
        },
        methods: {
            reconnect() {
                console.log('尝试重连')
                if (this.lockReconnect || this.maxReconnect <= 0) {
                    return
                }
                this.lockReconnect = true
                setTimeout(() => {
                    // this.maxReconnect-- // 不做限制 连不上一直重连
                    this.initWebSocket()
                    this.lockReconnect = false
                }, 60 * 1000)
            },
            initWebSocket() {
                try {
                    if ('WebSocket' in window) {
                        this.socket = new WebSocket(this.wsuri)
                    } else {
                        console.log('您的浏览器不支持websocket')
                    }
                    this.socket.onopen = this.websocketonopen
                    this.socket.onerror = this.websocketonerror
                    this.socket.onmessage = this.websocketonmessage
                    this.socket.onclose = this.websocketclose
                } catch (e) {
                    this.reconnect()
                }
            },
            websocketonopen() {
                console.log('WebSocket连接成功', this.socket.readyState)
                    // 发送心跳
                heartCheck.start(this.socket)
                    // 发送页面数据
                this.websocketsend()
            },
            websocketonerror(e) {
                console.log('WebSocket连接发生错误', e)
                this.reconnect()
            },
            websocketonmessage(e) {
                //不管拿到心跳包还是正常返回的数据  拿到任何消息都说明当前连接是正常的  处理数据时候 可以进行判断
                // 消息获取成功,重置心跳
                heartCheck.start(this.socket)

                switch (data.ModeCode) {
                    case "message":
                        console.log("收到消息" + data.msg)
                        break
                    case "heart_beat":
                        console.log(`收到心跳${data.msg}`)
                        break
                }

            },
            websocketclose(e) {
                console.log('connection closed (' + e.code + ')')
                this.reconnect()
            },
            websocketsend() {
                let data = {
                    id: 'a1b2c3'
                }
                this.socket.send(JSON.stringify({
                    ModeCode: ModeCode.MSG, //消息类型
                    msg: data
                }))
            }
        },
        destroyed() {
            this.socket.close()
        }
    }
</script>
<style lang="scss" scoped></style>




心跳机制 预防客户端网络断开时候只有在send发送数据时候才会触发close  和服务端异常时候客户端不能感知到 这时候也只有send发送后超时未接受到返回的信息时 触发close  两者都需要发送send信息 才能触发close  所以定时发送心跳

发送心跳是在初始ws连接成功后 直接发送心跳 然后在接收到也就是onmessage时候重置心跳(上面的start方法) 如果心跳发送出去一直没有返回信息 当超过心跳超时时间时候 就会触发close方法 启动重连

重连里(reconnect方法)可以设置重连条件限制 比如重连次数 超过次数直接return 或者有的条件不想重连了 可以加个开关(lockReconnect) 重连实际也就是再次执行initWebsocket

启动start重连  会定时(settimeout几秒后)发送send心跳包 如果超时settimeout几秒后还没收到就会触发close  但是要是在超时时间内收到了(onmessage里接受到了) 就会重新执行start 重新执行时候也就顺带把上一次的定时器清空了(一个是发送心跳定时器 一个是返回心跳信息的超时定时器)


class封装

 * @desc websocket 类
 * @param {String} path 连接地址
 * @param {Function} onmessage 连接地址
 * @return 
 * @author ppc
 * @date 2022-08-29 11:20:58
 */
import {
  Message
} from 'element-ui'
export class dsWebSocket {
  constructor(params) {
    this.params = params
    this.wesock = null // 建立的连接
    this.lockReconnect = false // 是否真正的建立连接
    this.timeout = 10 * 1000, // n秒一次的心跳
    this.timeoutObj = null // 心跳倒计时
    this.serverTimeoutObj = null // 心跳倒计时
    this.timeoutnum = null // 断开 重连倒计时
    console.log(this,'this constructor')
    if ('WebSocket' in window) {
      this.init(params)
    } else {
      Message({
        message: "浏览器不支持,请换浏览器再试",
        type: "error"
      });
    }
  }

  /**
   * @desc 初始化
   * @param 
   * @return 
   * @author ppc
   * @date 2022-08-29 11:21:21
   */
  init(params) {
    //建立连接
    this.websock = new WebSocket(params.path)
    // 连接成功
    this.websock.onopen = this.onOpen.bind(this)
    //连接错误
    this.websock.onerror = this.onError.bind(this)
    //接收信息
    this.websock.onmessage = this.getInformation.bind(this)

    //连接关闭
    this.websock.onclose = function () {}
    this.websock.onclose = this.close.bind(this)
  }

  /**
   * @desc 重新连接
   * @param 
   * @return 
   * @author ppc
   * @date 2022-08-29 11:37:36
   */
  reconnect() {
    if (this.lockReconnect) {
      return
    }
    this.lockReconnect = true;
    //没连接上会一直重连,设置延迟避免请求过多
    this.timeoutnum && clearTimeout(this.timeoutnum);
    this.timeoutnum = setTimeout(function () {
      //新连接
      this.init();
      this.lockReconnect = false;
    }, 5000);
  }

  /**
   * @desc 重置心跳
   * @param 
   * @return 
   * @author ppc
   * @date 2022-08-29 11:38:05
   */
  reset() {
    console.log('this',this)

    //清除时间
    clearTimeout(this.timeoutObj);
    clearTimeout(this.serverTimeoutObj);
    
    //重启心跳
    console.log('-----重启心跳------')
    this.start();
  }

  /**
   * @desc 开启心跳
   * @param 
   * @return 
   * @author ppc
   * @date 2022-08-29 11:38:30
   */
  start() {
    this.timeoutObj && clearTimeout(this.timeoutObj);
    this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
    this.timeoutObj = setTimeout( () =>{
      //这里发送一个心跳,后端收到后,返回一个心跳消息,
      if (this.websock&&this.websock.readyState == 1) {
        // console.log(222222)
        //如果连接正常
        this.websock.send('{"wsRequestType":"HEARTBEAT"}');
        // console.log("发送消息");
      } else {
        //否则重连
        // console.log("重新链接")
        // this.reconnect();
      }
      this.serverTimeoutObj = setTimeout(function () {
        //超时关闭
        this.websock.close();
      }, this.timeout);
    }, this.timeout);
  }

  /**
   * @desc 连接成功
   * @param 
   * @return 
   * @author ppc
   * @date 2022-08-29 11:39:32
   */
  onOpen() {
    //提示成功
    // this.isOnline = 1;
    //开启心跳
    console.log(this,'this')
    console.log('-----连接成功------')
    this.start();
  }

  /**
   * @desc 连接失败
   * @param 
   * @return 
   * @author ppc
   * @date 2022-08-29 11:41:06
   */
  onError() {
    // console.log("WebSocket连接发生错误");
    // this.isOnline = 2;
    //重连
    this.reconnect();
  }

  /**
   * @desc 连接关闭事件
   * @param 
   * @return 
   * @author ppc
   * @date 2022-08-29 11:41:27
   */
  close() {
    // this.reconnect();
  }

  /**
   * @desc 接收服务器推送的信息
   * @param 
   * @return 
   * @author ppc
   * @date 2022-08-29 11:41:59
   */
  getInformation(event) {
    //收到服务器信息,心跳重置
    this.reset();
    const redata = JSON.parse(event.data);
    this.params.onmessage(redata)
    return redata
  }
  /**
   * @desc 向服务器发送信息
   * @param 
   * @return 
   * @author ppc
   * @date 2022-08-29 11:43:33
   */
  send() {

  }

}


     var that = this;
            const webSocket = new dsWebSocket({
                path: wsuri,
                onmessage: function (msg) {
                    console.log(msg, "返回的数据);
                    //数据处理
                    that.websocketonmessage(msg);
                },
            });


在onopen时候发送心跳,在onmessage时候晴空心跳定时器和心跳超时定时器,但是如果心跳超时会直接调用close传递自定义code(相当于非正常关闭 !1000),然后在onclose事件里面判断假如非正常关闭,启动重连(可以设置重连最大次数)正常手动close时候需要socket设为null,自定义状态为Disconnected