websocket心跳的實現(包括全部程式碼)
阿新 • • 發佈:2019-02-18
本文主要講的是如果設計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;
}
}
}
}
}
以上就是websocket心跳的全部實現。看到斷開網路後然後再臉上網路websocket又連上了,那一刻心裡很喜悅。如果有什麼問題,歡迎大家和我交流。
郵箱: 1227620310@qq.com