本文主要講的是如果設計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
- 1
- 2
- 3