本文主要讲的是如果设计websocket心跳已经需要考虑哪些问题。
前言
在使用websocket的过程中,有时候会遇到客户端网络关闭的情况,而这时候在服务端并没有触发onclose事件。这样会:
- 多余的连接
- 服务端会继续给客户端发数据,这些数据会丢失
所以就需要一种机制来检测客户端和服务端是否处于正常连接的状态。这就是websocket心跳,这个名字非常生动形象,还有心跳说明还活着(保持正常连接),没有心跳说明已经挂掉了(连接断开了)。
要解决的问题
我的代码主要解决了以下几个问题。
- 连接上之后,每秒发送一个心跳,服务器同样返回一个心跳,用来表示服务器没挂。
- 断线重连(我们测试的环境是断开网络连接),断开网络后,心跳包无法发送出去,所以如果当前时间距离上次成功心跳的时间超过20秒,说明连接已经出现问题了,此时需要关闭连接。
- 第一次关闭连接时websocket会尝试重连,设置了一个时间期限,10秒。10秒内如果能连上(恢复网络连接)就可以继续收发消息,连不上就关闭了,并且不会重连。
- 30秒内收不到服务器消息(心跳每秒发送),我就认为服务器已经挂了,就会调用close事件,然后进入第3步。
需要什么
开始考虑得不周到,命名不规范。
- 一个定时器
ws.keepAliveTimer
,用来每秒发送一次心跳。 - 上次心跳成功的时间
ws.last_health_time
以及当前时间let time = new Date().getTime();
。 - 断开连接(
ws.close()
)时的时间reconnect
,因为在close事件发生后需要重连10秒。 - 是否已经重连过
reconnectMark
。 - 断开连接(
ws.close()
)时需要保存ws对象tempWs
。我曾试图ws = { ...ws }
发现会丢失绑定的事件。 - 一个定时时间为30秒的setTimeout定时器
ws.receiveMessageTimer
,用来表示服务器是否在30秒内返回了消息。
代码部分
我是在react中使用websocket心跳的。当用户登录时我会建立websocket连接。由于使用了redux,所以该部分代码放在componentWillReceiveProps
中。
componentWillReceiveProps(nextProps) {
if(nextProps.isLogin && !this.state.notificationSocket) { // 用户登录了并且没有连接过websocket
let ws = new WebSocket(`${chatUrl}/${nextProps.userId}`);
ws.last_health_time = -1; // 上一次心跳时间
ws.keepalive = function() {
let time = new Date().getTime();
if(ws.last_health_time !== -1 && time - ws.last_health_time > 20000) { // 不是刚开始连接并且20s
ws.close()
} else {
// 如果断网了,ws.send会无法发送消息出去。ws.bufferedAmount不会为0。
if(ws.bufferedAmount === 0 && ws.readyState === 1) {
ws.send('h&b');
ws.last_health_time = time;
}
}
}
if(ws) {
let reconnect = 0; //重连的时间
let reconnectMark = false; //是否重连过
this.setState({
notificationSocket: true
})
ws.onopen = () => {
reconnect = 0;
reconnectMark = false;
ws.receiveMessageTimer = setTimeout(() => {
ws.close();
}, 30000); // 30s没收到信息,代表服务器出问题了,关闭连接。如果收到消息了,重置该定时器。
if(ws.readyState === 1) { // 为1表示连接处于open状态
ws.keepAliveTimer = setInterval(() => {
ws.keepalive();
}, 1000)
}
}
ws.onerror = () => {
console.error('onerror')
}
ws.onmessage = (msg) => {
/* 这一注释部分是我的业务逻辑代码,大家可以忽略
msg = JSON.parse(msg.data);
let chatObj = JSON.parse(localStorage.getItem(CHATOBJECT)) || {};
if(msg && msg.senderUserId && !chatObj[msg.senderUserId]) chatObj[msg.senderUserId] = [];
if(msg.content !== 'h&b') {
if(msg.chat === true) { // 聊天
// chatObj[msg.senderUserId] = [<p key={new Date().getTime()}>{msg.content}</p>, ...chatObj[msg.senderUserId]]
chatObj[msg.senderUserId].unshift(msg.content);
WindowNotificationUtils.notice(msg.title, msg.content, () => {
const { history } = this.props;
history.replace({
pathname: '/sendNotice',
search: `?senderUserId=${msg.senderUserId}` // 为什么放在url,因为刷新页面数据不会掉
});
})
localStorage.setItem(CHATOBJECT, JSON.stringify(chatObj));
this.props.dispatch({
type: UPDATE_CHAT
})
} else { // 通知
WindowNotificationUtils.notice(msg.title, msg.content);
}
}
*/
// 收到消息,重置定时器
clearTimeout(ws.receiveMessageTimer);
ws.receiveMessageTimer = setTimeout(() => {
ws.close();
}, 30000); // 30s没收到信息,代表服务器出问题了,关闭连接。
}
ws.onclose = () => {
clearTimeout(ws.receiveMessageTimer);
clearInterval(ws.keepAliveTimer);
if(!reconnectMark) { // 如果没有重连过,进行重连。
reconnect = new Date().getTime();
reconnectMark = true;
}
let tempWs = ws; // 保存ws对象
if(new Date().getTime() - reconnect >= 10000) { // 10秒中重连,连不上就不连了
ws.close();
} else {
ws = new WebSocket(`${chatUrl}/${nextProps.userId}`);
ws.onopen = tempWs.onopen;
ws.onmessage = tempWs.onmessage;
ws.onerror = tempWs.onerror;
ws.onclose = tempWs.onclose;
ws.keepalive = tempWs.keepalive;
ws.last_health_time = -1;
}
}
}
}
}
- 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
- 87
- 88
- 89
- 90
- 91
- 92