websocket心跳重连机制

# websocket心跳重连机制

在使用websocket的过程中,有时候会遇到网络断开的情况,但是在网络断开的时候服务器端并没有触发onclose的事件。这样会有:服务器会继续向客户端发送多余的链接,并且这些数据还会丢失。所以就需要一种机制来检测客户端和服务端是否处于正常的链接状态。因此就有了websocket的心跳了。还有心跳,说明还活着,没有心跳说明已经挂掉了。

# 为什么叫心跳包呢?

它就像心跳一样每隔固定的时间发一次,来告诉服务器,我还活着。

# 心跳机制

心跳机制是每隔一段时间会向服务器发送一个数据包,告诉服务器自己还活着,同时客户端会确认服务器端是否还活着,如果还活着的话,就会回传一个数据包给客户端来确定服务器端也还活着,否则的话,有可能是网络断开连接了。需要重连~

export default {
    name: 'App',
    data () {
        return {
            isOpen:false,//是否连接
            pingIntervalSeconds:3000,//心跳连接时间
            lockReconnect: false,//是否真正建立连接
            heartTimer: null,//心跳定时器
            serverTimer: null,//服务器超时 定时器
            reconnectTimer: null,//断开 重连倒计时
            sendFixHeartTimer:null,//20s固定发送心跳定时器
        }
    },
    created(){
        this.connect();
    },
    methods:{
        // ws连接
        connect(){
            ws.WebSocket('wss://ws.xx.com/xx','');
            // 监听连接开启,
            ws.onopen(e => {
                //开启心跳
                this.start();
                this.sendFixHeart();
            });
            ws.onmessage(e => {
                // 收到服务器信息,心跳重置,上报
                this.reset();
            });
            ws.onerror(e => {
                //重连
                this.reconnect();
            });
            ws.onclose(e => {
                //重连
                this.reconnect();
            });
        },
        //开启心跳
        start(){
            this.heartTimer && clearTimeout(this.heartTimer);
            this.serverTimer && clearTimeout(this.serverTimer);
            this.heartTimer = setTimeout(()=>{
                this.send({
                    cmd:1100,
                });
                //超时关闭,超时时间为5s
                this.serverTimer = setTimeout(()=>{
                    ws.close();
                }, 5000);
            }, this.pingIntervalSeconds)
        },
        //重新连接  3000-5000之间,设置延迟避免请求过多
        reconnect(){
            //设置lockReconnect变量避免重复连接
            if(this.lockReconnect) return;
            this.lockReconnect = true;
            this.reconnectTimer && clearTimeout(this.reconnectTimer);
            this.reconnectTimer = setTimeout(()=> {
                this.connect();
                this.lockReconnect = false;
            }, parseInt(Math.random()*2000 + 3000));
        },
        //重置心跳
        reset(){
            clearTimeout(this.heartTimer);
            clearTimeout(this.serverTimer);
            this.start();
        },
        // 20s固定发送心跳
        sendFixHeart(){
            clearInterval(this.sendFixHeartTimer);
            this.sendFixHeartTimer = setInterval(()=>{
                this.send({
                    cmd:1100,
                });
            }, 20000);
        }

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
var lockReconnect = false; // 避免重复连接
var wsUrl = 'wss://echo.websocket.org';
var ws;
var tt;
function createWebSocket() {
 try {
  ws = new WebSocket(wsUrl);
  init();
 } catch(e) {
  console.log('catch');
  reconnect(wsUrl);
 }
}

function init() {
 ws.onclose = function() {
  reconnect(wsUrl);
 }
 ws.onerror = function() {
  reconnect(wsUrl)
 }
 ws.onopen = function() {
  heartCheck.start();
 }
 ws.onmessage = function(event) {
  heartCheck.start()
 }
}

function reconnect(url) {
 if(lockReconnect) {
  return;
 };
 lockReconnect = true;
 tt && clearTimeout(tt);
 tt = setTimeout(function() {
  createWebSocket(url);
  lockReconnect = false;
 }, 4000)
}

// 心跳检测
var heartBeat = {
 timeout: 3000,
 timeoutObj: null,
 serverTimeoutObj: null,
 start: function() {
  var self = this;
  this.timeoutObj && clearTimeout(this.timeoutObj);
  this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
  this.timeoutObj = setTimeout(function() {
   ws.send('xxx')
   self.serverTimeoutObj = setTimeout(function() {
    ws.close()
   }, self.timeout)
  }, this.timeout)
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

WebSocket 是一种网络通信协议,它使得客户端和服务器之间的数据交换变得更加简单。

WebSocket 允许服务端主动向客户端推送数据。之前很多网站为了实现推送技术,采用的技术都是轮询,不仅效率低,也浪费了很多带宽等资源。HTML5 定义了 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

WebSocket 的优势:

  • 较少的控制开销
  • 更强的实时性
  • 保持连接状态
  • 更好的二进制支持
  • 可以支持扩展
  • 更好的压缩效果

WebSocket 最大的优势就是能够保持前后端消息的长连接,但是在某些情况下,长连接失效并不会得到及时的反馈,前端并不知道连接已断开。例如用户网络断开,并不会触发 websocket 的任何事件函数,这个时候如果发送消息,消息便无法发送出去,浏览器会立刻或者一定短时间后(不同浏览器或者浏览器版本可能表现不同)触发 onclose 函数。

为了避免这种情况,保证连接的稳定性,前端需要进行一定的优化处理,一般采用的方案就是心跳重连。前后端约定,前端按一定间隔发送一个心跳包,后端接收到心跳包后返回一个响应包,告知前端连接正常。如果一定时间内未接收到消息,则认为连接断开,前端进行重连。

# 心跳重连

通过以上分析,可以得到实现心跳重连的关键是按时发送心跳消息和检测响应消息并判断是否进行重连,所以首先设置 4 个小目标:

  • 可以按一定间隔发送心跳包
  • 连接错误或者关闭时能够自动重连
  • 若在一定时间间隔内未接收消息,则视为断连,自动进行重连
  • 可以自定义心跳消息并设置最大重连次数

工具类 WebsocketHB

class WebsocketHB {
  constructor({
    url, // 连接客户端地址
    pingTimeout = 8000, // 发送心跳包间隔,默认 8000 毫秒
    pongTimeout = 15000, // 最长未接收消息的间隔,默认 15000 毫秒
    reconnectTimeout = 4000, // 每次重连间隔
    reconnectLimit = 15, // 最大重连次数
    pingMsg // 心跳包的消息内容
  }) {
    // 初始化配置
    this.url = url
    this.pingTimeout = pingTimeout
    this.pongTimeout = pongTimeout
    this.reconnectTimeout = reconnectTimeout
    this.reconnectLimit = reconnectLimit
    this.pingMsg = pingMsg

    this.ws = null
    this.createWebSocket()
  }

  // 创建 WS
  createWebSocket() {
    try {
      this.ws = new WebSocket(this.url)
      this.ws.onclose = () => {
        this.onclose()
      }
      this.ws.onerror = () => {
        this.onerror()
      }
      this.ws.onopen = () => {
        this.onopen()
      }
      this.ws.onmessage = event => {
        this.onmessage(event)
      }
    } catch (error) {
      console.error('websocket 连接失败!', error)
      throw error
    }
  }

  // 发送消息
  send(msg) {
    this.ws.send(msg)
  }
}


const ws = new WebsocketHB({
  url: 'ws://xxx'
})

ws.onopen = () => {
  console.log('connect success')
}
ws.onmessage = e => {
  console.log(`onmessage: ${e.data}`)
}
ws.onerror = () => {
  console.log('connect onerror')
}
ws.onclose = () => {
  console.log('connect onclose')
}
ws.send('Hello, chanshiyu!')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

# 发送心跳包与重连

这里使用 setTimeout 模拟 setInterval 定时发送心跳包,避免定时器队列阻塞,并且限制最大重连次数。需要注意的是每次进行重连时加锁,避免进行无效重连,同时在每次接收消息时,清除最长间隔消息重连定时器,能接收消息说明连接正常,不需要重连。

class WebsocketHB {
  constructor() {
    // ...
    // 实例变量
    this.ws = null
    this.pingTimer = null // 心跳包定时器
    this.pongTimer = null // 接收消息定时器
    this.reconnectTimer = null // 重连定时器
    this.reconnectCount = 0 // 当前的重连次数
    this.lockReconnect = false // 锁定重连
    this.lockReconnectTask = false // 锁定重连任务队列

    this.createWebSocket()
  }

  createWebSocket() {
    // ...
    this.ws = new WebSocket(this.url)
    this.ws.onclose = () => {
      this.onclose()
      this.readyReconnect()
    }
    this.ws.onerror = () => {
      this.onerror()
      this.readyReconnect()
    }
    this.ws.onopen = () => {
      this.onopen()

      this.clearAllTimer()
      this.heartBeat()
      this.reconnectCount = 0
      // 解锁,可以重连
      this.lockReconnect = false
    }
    this.ws.onmessage = event => {
      this.onmessage(event)

      // 超时定时器
      clearTimeout(this.pongTimer)
      this.pongTimer = setTimeout(() => {
        this.readyReconnect()
      }, this.pongTimeout)
    }
  }

  // 发送心跳包
  heartBeat() {
    this.pingTimer = setTimeout(() => {
      this.send(this.pingMsg)
      this.heartBeat()
    }, this.pingTimeout)
  }

  // 准备重连
  readyReconnect() {
    // 避免循环重连,当一个重连任务进行时,不进行重连
    if (this.lockReconnectTask) return
    this.lockReconnectTask = true
    this.clearAllTimer()
    this.reconnect()
  }

  // 重连
  reconnect() {
    if (this.lockReconnect) return
    if (this.reconnectCount > this.reconnectLimit) return

    // 加锁,禁止重连
    this.lockReconnect = true
    this.reconnectCount += 1
    this.createWebSocket()
    this.reconnectTimer = setTimeout(() => {
      // 解锁,可以重连
      this.lockReconnect = false
      this.reconnect()
    }, this.reconnectTimeout)
  }}

  // 清空所有定时器
  clearAllTimer() {
    clearTimeout(this.pingTimer)
    clearTimeout(this.pongTimer)
    clearTimeout(this.reconnectTimer)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

# 实例销毁

最后给工具类加一个销毁方法,在实例销毁的时候设置一个禁止重连锁,避免销毁的时候还在尝试重连,并且清空所有定时器,关闭长连接。

class WebsocketHB {
  // 重连
  reconnect() {
    if (this.forbidReconnect) return
    //...
  }

  // 销毁 ws
  destroyed() {
    // 如果手动关闭连接,不再重连
    this.forbidReconnect = true
    this.clearAllTimer()
    this.ws && this.ws.close()
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
上次更新: 2022/6/30 下午8:45:35