理解WebSocket心跳重連機制以及移動端鎖屏或退到後臺運行時重連機制

一 WebSocket 心跳重連機制

在使用websocket的過程中,有時候會遇到網絡斷開的情況,但是在網絡斷開的時候服務器端並沒有觸發onclose的事件。這樣會有:服務器會繼續向客戶端發送多餘的鏈接,並且這些數據還會丟失。所以就需要一種機制來檢測客戶端和服務端是否處於正常的鏈接狀態。因此就有了websocket的心跳了。還有心跳,說明還活着,沒有心跳說明已經掛掉了。

1. 爲什麼叫心跳包呢?
它就像心跳一樣每隔固定的時間發一次,來告訴服務器,我還活着。

2. 心跳機制是?
心跳機制是每隔一段時間會向服務器發送一個數據包,告訴服務器自己還活着,同時客戶端會確認服務器端是否還活着,如果還活着的話,就會回傳一個數據包給客戶端來確定服務器端也還活着,否則的話,有可能是網絡斷開連接了。需要重連~

前臺代碼:

var websocket;

// 設置端口號
var serverPort = '8080';

// 設置路徑
var url = '/platform-framework/api/osWebsocket';

// 獲取IP地址
function getWebIP() {
    var curIP = window.location.hostname;
    return curIP;
}


function init(){

    //ws地址
    var wsuri = "ws://" + getWebIP() + ":" + serverPort + url;
    try {
        websocket = new WebSocket(wsuri);
    } catch (e) {
        console.log(' 您的瀏覽器暫時不支持 webSocket ');
    }

    websocket.onclose = function (e) {
        websocket.close();
        console.log("WebSocket 關閉");
    }
    
    websocket.onopen = function () {
        //心跳檢測重置
    	heartCheck.start();
    }

    //連接發生錯誤的回調方法
    websocket.onerror = function () {
        websocket.close();
        console.log("WebSocket 連接發生錯誤");
    }

}

// 初始化
init();

/***
 *	webSocket 自動接收數據
 *  給外部預留接口,方便頁面實時獲取新內容
 *  傳遞方法(messageCallback)進來,給方法賦返回值(websocket接收到的數據)
 */
function onMessage(messageCallback) { 
    websocket.onmessage = function (e) {
        if (typeof(messageCallback) === 'function') {
            messageCallback(JSON.parse(e.data));
        }
        // 心跳檢測重置
        heartCheck.reset();
    }
}

// webSocket 發送消息方法
function sendSockTable(agentData) {
	if (websocket.readyState === websocket.OPEN) {
        //若是ws開啓狀態
        websocketsend(agentData)
    } else if (websocket.readyState === websocket.CONNECTING) {
        // 若是 正在開啓狀態,則等待1s後重新調用
        setTimeout(function () {
            sendSockTable(agentData);
        }, 1000);
    } else {
        // 若未開啓 ,則等待1s後重新調用
        setTimeout(function () {
            sendSockTable(agentData);
        }, 1000);
    }
}

// 數據發送
function websocketsend(agentData) {
    websocket.send(JSON.stringify(agentData));
}

// webSocket 關閉
function webSocketClose() {
	websocket.close();
}

// webSocket 初始化
function webSocketInit() {
	init();
}

// 關閉並重新鏈接
function webSocketRestart() {
    websocket.close();
    init();
}

// 強制關閉瀏覽器  調用websocket.close(),進行正常關閉
window.onunload = function() {
   websocket.close();
}

// 心跳檢測 超過60秒之後會判定後端主動斷開了
var heartCheck = {
  timeout: 60000,
  timeoutObj: null,
  reset: function(){
    clearTimeout(this.timeoutObj);
    this.start();
  },
  start: function(){
    this.timeoutObj = setTimeout(function () {
      console.info("客戶端發送心跳:" + new Date());
      websocket.send('心跳鏈接測試');
    }, this.timeout)
  }
}

export default { onMessage,sendSockTable, webSocketRestart, webSocketClose, webSocketInit}

具體的思路如下:

1. 第一步頁面初始化,調用當前 js 時,自動調用 init()函數,目的是創建一個websocket的方法,並初始化websocket一些方法

2. 第二步調用 onMessage((response) = >{ }) 函數,獲取webSocket自動推送消息;調用sendSockTable(數據)函數,發送消息,調用webSocketRestart()函數,重啓webSocket

3. 如上,當網絡斷開的時候,會先調用onerror,onclose事件可以監聽到,會進行相應操作。正常的情況下,是先調用onopen方法的,當接收到數據時,會被onmessage事件監聽到。

4. 最後一步就是實現心跳檢測的代碼

後臺代碼:

package com.platform.api;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.websocket.EncodeException;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import org.apache.log4j.Logger;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

//import org.apache.log4j.Logger;

import com.alibaba.fastjson.JSONObject;
import com.github.pagehelper.util.StringUtil;
import com.platform.entity.OsOrderInfoVo;
import com.platform.entity.OsTableManagement;
import com.platform.entity.OsWebSocketVo;
import com.platform.service.OsOrderService;
import com.platform.service.OsTableManagementService;
import com.platform.util.ApplicationContextRegister;
import com.platform.util.ServerEncoder;

/**
 * @ServerEndpoint 註解是一個類層次的註解,它的功能主要是將目前的類定義成一個websocket服務器端,
 * 註解的值將被用於監聽用戶連接的終端訪問URL地址,客戶端可以通過這個URL來連接到WebSocket服務器端
 */
@Component
@ServerEndpoint(value = "/api/websocket", encoders = { ServerEncoder.class })
public class OsWebSocketController {

	private Logger log = Logger.getLogger(OsWebSocketController.class);
	
	//靜態變量,用來記錄當前在線連接數。應該把它設計成線程安全的。
	private static int onlineCount = 0;

	//concurrent包的線程安全Set,用來存放每個客戶端對應的MyWebSocket對象。若要實現服務端與單一客戶端通信的話,可以使用Map來存放,其中Key可以爲用戶標識
	private static CopyOnWriteArraySet<OsWebSocketController> webSocketSet = new CopyOnWriteArraySet<OsWebSocketController>();
	
	private static Map<String, List<OsWebSocketVo>> map = new HashMap<String, List<OsWebSocketVo>>();

	//與某個客戶端的連接會話,需要通過它來給客戶端發送數據
	private Session session;
	
	/**
	 * 連接建立成功調用的方法a
	 * @param session  可選的參數。session爲與某個客戶端的連接會話,需要通過它來給客戶端發送數據
	 * @throws IOException  
	 */
	@OnOpen
	public void onOpen(Session session) throws IOException{
		this.session = session;
		webSocketSet.add(this);
		addOnlineCount(); 
		log.info("OsWebSocketController -> 有新連接加入!當前在線人數爲" + getOnlineCount());
	}

	/**
	 * 連接關閉調用的方法
	 */
	@OnClose
	public void onClose(){
		webSocketSet.remove(this);
		subOnlineCount();
		log.info("OsWebSocketController -> 有一連接關閉!當前在線人數爲" + getOnlineCount());
	}

	/**
	 * 收到客戶端消息後調用的方法
	 * @param message 客戶端發送過來的消息
	 * @param session 可選的參數
	 */
	@OnMessage
	public void onMessage(String message, Session session) {

        /***
         *  心跳測試只是爲了證明其連通性
         *  因 前臺傳遞websocket時,心跳測試和普通數據結構不同,所以在此判斷,
         *  可在其不是心跳測試時,做其他操作
         */   
		log.info("【WebSocket消息】接收心跳:{}"+ message);
		if("心跳鏈接測試".equals(message)) {
			
		} else {
			// 可以添加
            // OsWebSocketGoods vo = JSONObject.parseObject(message, OsWebSocketGoods.class);
		}
		
		//羣發消息
		for(OsWebSocketController item: webSocketSet){
			try {
				item.sendMessageTo(map);
			} catch (IOException e) {
				e.printStackTrace();
				continue;
			} catch (EncodeException e) {
				e.printStackTrace();
				continue;
			}
		}
	}
	
	/**
	 * 發生錯誤時調用
	 * @param session
	 * @param error
	 */
	@OnError
	public void onError(Session session, Throwable error){
		log.info("OsWebSocketController -> 發生錯誤!");
		log.info("OsWebSocketController -> 錯誤信息:" + error.getMessage());
		error.printStackTrace();
	}

	/**
	 * 這個方法與上面幾個方法不一樣。沒有用註解,是根據自己需要添加的方法。
	 * @param message
	 * @throws IOException
	 */
	public void sendMessage(String message) throws IOException{
		this.session.getBasicRemote().sendText(message);
	}
	
	public void sendMessageTo(Map<String, List<OsWebSocketVo>> map) throws IOException, EncodeException {
		this.session.getBasicRemote().sendObject(map);
	}

	public static synchronized int getOnlineCount() {
		return onlineCount;
	}

	public static synchronized void addOnlineCount() {
		OsWebSocketController.onlineCount++;
	}

	public static synchronized void subOnlineCount() {
		OsWebSocketController.onlineCount--;
	}
	
	
}

實現心跳檢測的思路是:每隔一段固定的時間,向服務器端發送一個ping數據,如果在正常的情況下,服務器會返回一個pong給客戶端,如果客戶端通過onmessage事件能監聽到的話,說明請求正常,這裏我們使用了一個定時器,每隔3秒(時間自定義)的情況下,如果是網絡斷開的情況下,在指定的時間內服務器端並沒有返回心跳響應消息,因此服務器端斷開了,因此這個時候我們使用websocket.close關閉連接,在一段時間後(在不同的瀏覽器下,時間是不一樣的,firefox響應更快),可以通過 onclose事件監聽到。因此在onclose事件內,我們可以調用 reconnect事件進行重連操作。

二 WebSocket 移動端鎖屏以及退到後臺重連機制

js在手機熄屏後會中斷,在喚醒之後js會繼續執行。所以設置在js中的定時發送心跳包的功能在手機熄屏後就沒法執行了。熄屏時間過長,這個時候鏈接就會被服務端強制斷開,並且大部分手機在熄屏時,websocket連接就已經斷開了。

解決辦法: 使用H5提供的頁面隱藏/顯示API。


document.addEventListener('visibilitychange',function() {
	if(document.visibilityState == 'hidden') {
		//記錄頁面隱藏時間
		let hiddenTime = new Date().getTime()	
	} else {
		let visibleTime = new Date().getTime();
		//頁面再次可見的時間-隱藏時間>10S,重連	
		if((visibleTime - hiddenTime) / 1000 > 10){	
			// 主動關閉連接
			WebSockets.webSocketClose();
			// 1.5S後重連 因爲斷開需要時間,防止連接早已關閉了
			setTimeout(function(){
				WebSockets.openSocket()   
			},1500);    
		}else{
 			console.log('還沒有到斷開的時間')
		}
	}
}

此方法不僅適用熄屏後重連,也適用於手機瀏覽器被切換至後臺運行時js中斷的情況

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章