Websocket技術的Java實現(下篇)

    在上篇中,我們探討了使用Java實現websocket的方式。但顯然,上篇的實現方式還有一些細節的東西需要打磨一下。那麼在下篇中,我們就一起優化一下上篇中的實現方式吧。

用戶sid的指定

    在上篇中,我們用sid來區分每個用戶(也可以理解爲每個網頁窗口)。但sid是由前端頁面在發送websocket請求時隨請求過來的,如果由用戶自行指定sid,那很容易出現sid重複的情況(也就是串線)。所以需要由程序來指定每個用戶的sid。
    sid值可以由前端設置,也可以由後端指定再返回給前端。
    如果websocket同時在線用戶數比較少,對重複性要求不高的情況下,可以直接在前端使用隨機數算法:

function random(lower, upper) {
	return Math.floor(Math.random() * (upper - lower+1)) + lower;
}

    在打開一個websocket頁面窗口時,調用隨機數算法指定該窗口的用戶sid。比如:

var sid = random(1,10000)	// 在1-10000中生成隨機數

    不過顯然,在前端使用隨機數算法仍不可避免的出現sid重複性的問題。如果用戶數在線數比較多的話,可以用uuid作爲代替。
    除了在前端指定sid以外,也可以首次請求的時候不帶sid,由後端指定sid返回給前端,前端再綁定sid。這裏可以由後端分發uuid或者用雪花算法計算的值,也可以通過後端維護一個sid的稽覈,這裏就不展開來講,如果大家有興趣,可以自己動手試試。

斷線重連機制

斷線重連機制下WebSocketServer類調整

    因網絡原因或者其他原因,有可能會出現斷線的情況,而客戶端或者服務器沒有監測機制,無法得知連接是否保持。這時候需要建立斷線重連機制(也可以說心跳重連機制)。
    簡單而言,就是設置定時器,每隔一定時間發送一次特定的信息(心跳),當服務器返回任何信息(包括心跳回復或信息推送),都證明連接可靠,此時重置定時器。
    我們首先調整一下服務器端WebSocketServer類,在onMessage方法中增加心跳相關的代碼。作爲心跳的特定字符串不能爲常見的字符串,以免服務端將正常的信息和心跳混淆。

@ServerEndpoint(value = "/ws/{sid}")
@Component
public class WebSocketServer {

	......
	
	// 根據心跳機制調整onMessage方法
	@OnMessage
    public void onMessage(String message, Session session) {
        // 對心跳進行回覆(回覆自己)
        if (message.equals("$$")) {	 // 心跳爲$$
            try {
                sendInfo("1$",sid);	// 假定心跳返回的字符串爲1$
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            // 對正常信息進行處理
            log.info("收到來自窗口" + sid + "的信息:" + message);
//        if(StringUtils.isNotBlank(message)){
//            for(WebSocketServer server:websocketMap.values()) {
//                try {
//                    server.sendMessage(message);
//                } catch (IOException e) {
//                    e.printStackTrace();
//                    continue;
//                }
//            }
//        }
            try {
                //從message中解析出toSid和content
                Map map = JSONObject.parseObject(message, Map.class);
                String toSid = (String) map.get("toSid");
                String content = "sid" + sid + ":" + (String) map.get("content");
                //驗證toSid是否上線。如果toSid爲"",視爲羣發
                if ("".equals(toSid) || websocketMap.get(toSid) != null) {
                    sendInfo(content, toSid);
                } else {
                    sendInfo(toSid + "未上線",sid);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

斷線重連機制下websocket的調用

4.1)提供接口進行消息推送

    同上篇

4.2)由前端指定ws調用網址直接使用

    在創建websocket連接的時候,要對該連接進行設置,在onclose和onerror方法中增加重連的方法。並建立心跳,通過一個定時器定時發送心跳字符串,如果返回指定的字符串,則認爲本次心跳成功,定時器重置,而長時間沒有返回指定的字符串,則觸發重連的方法。具體的ws頁面如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>websocket通訊2</title>
</head>
<script type="text/javascript" src="js/jquery-3.3.1.min.js"></script>
<script>
    var lockReconnect = false;//避免重複連接
    var ws;
    var tt;
    var sid = random(1,100000);    // 這裏將頁面的sid寫死,重連的時候會請求同樣的sid,容易出現重複性問題,最好是由後端提供去重後的sid
    var wsUrl = "ws://localhost:9010/javatest/ws/" + sid;		// websocket鏈接,項目地址

    function createWebSocket(){
        try {
            if(typeof(WebSocket) == "undefined") {
                console.log("您的瀏覽器不支持WebSocket");
            }else {
                console.log("您的瀏覽器支持WebSocket");
                console.log("sid:" + sid);
                ws = new WebSocket(wsUrl);
                websocketInit();
            }
        } catch (e) {
            console.log('catch');
            websocketReconnect(wsUrl);
        }
    }

    function websocketInit() {
        // 建立websocket連接成功觸發事件
        ws.onopen = function (evt) {
            onOpen(evt);
        };
        // 斷開websocket連接成功觸發事件
        ws.onclose = function (evt) {
            websocketReconnect(wsUrl);
            onClose(evt);
        };
        // 接收服務端數據時觸發事件
        ws.onmessage = function (evt) {
            onMessage(evt);
        };
        // 通信發生錯誤時觸發
        ws.onerror = function (evt) {
            websocketReconnect(wsUrl);
            onError(evt);
        };
    }
    
    function onOpen(evt) {
        console.log("websocket連接已建立");
        //心跳檢測重置
        heartCheck.start();
    }

    function onClose(evt) {
        console.log("websocket連接已關閉");
    }

    function onMessage(evt) {
        // 心跳不需要處理
        if (evt.data==="1$") {
            console.log('heartbeat!!!'); 
        } else {
            // 對後端返回信息做業務操作
            console.log('接收消息: ' + evt.data);
            var msg = $("#message").html() + evt.data+'\n';
            $("#message").html(msg);      // 根據業務需求調整
        }
        // 拿到信息說明連接正常,心跳重置
        heartCheck.start();
    }

    function onError(evt) {
        console.log("websocket連接發生錯誤:" + evt.data);
    }

    // 斷線重連
    function websocketReconnect(url) {
        if (lockReconnect) {       // 是否已經執行重連
            return;
        }
        lockReconnect = true;
        //沒連接上會一直重連,設置延遲避免請求過多
        tt && clearTimeout(tt);
        tt = setTimeout(function () {
            createWebSocket(url);
            lockReconnect = false;
        }, 5000);
    }

    // 心跳建立
    var heartCheck = {
        timeout: 30000,
        timeoutObj: null,
        serverTimeoutObj: null,
        start: function () {
            console.log('heartbeat...');    // 正式使用時要去掉
            var self = this;
            this.timeoutObj && clearTimeout(this.timeoutObj);
            this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
            this.timeoutObj = setTimeout(function () {
                //這裏發送一個心跳,後端收到後,返回一個心跳消息,
                //onmessage拿到返回的心跳就說明連接正常
                ws.send("$$");
                self.serverTimeoutObj = setTimeout(function () {//如果超過一定時間還沒重置,說明後端主動斷開了
                    ws.close();     //如果onclose會執行reconnect,我們執行ws.close()就行了.如果直接執行reconnect 會觸發onclose導致重連兩次
                }, self.timeout);
            }, this.timeout)
        }
    };

    // 主動發送消息
    function sendMessage() {
        if(typeof(WebSocket) == "undefined") {
            console.log("您的瀏覽器不支持WebSocket");
        }else {
            console.log("您的瀏覽器支持WebSocket");
            console.log('{"toSid":"'+$("#toSid").val()+'","content":"'+$("#content").val()+'"}');
            ws.send('{"toSid":"'+$("#toSid").val()+'","content":"'+$("#content").val()+'"}');
        }
    }

    // 創建隨機數作爲sid,但是存在重複性問題
    function random(lower, upper) {
        return Math.floor(Math.random() * (upper - lower+1)) + lower;
    }

    // 進入頁面後便建立websocket連接
    createWebSocket(wsUrl);
</script>
<body>
<p>【toSid】:<div><input id="toSid" name="toSid" type="text"/></div>
<p>【發送內容】:<div><input id="content" name="content" type="text" value="abc"/></div>
<p>【操作】:<div><input type="button" onclick="sendMessage()" value="發送消息"/><br/>
<p>【接收內容】:<div><textarea id="message"></textarea></div>
</body>
</html>

    注意:加入斷線重連機制後,由於onclose方法調用了重連的方法,所以無法在瀏覽器中手動關閉websocket。也就是說除了關閉網頁或者跳轉到其他網頁的情況以外,均無法徹底關閉websocket。

斷線重連機制總結

    其實加上心跳機制後,心跳檢測和傳統的http輪詢沒什麼太大的差別,都是定時發送請求。但是websocket仍然具有以下的優勢:時效性比輪詢強,屬於服務器主動推送;不需要頻繁創建http連接,減少資源的浪費。如果想減輕服務器的壓力,心跳間隔可以根據需求增大。

參考文獻:

理解WebSocket心跳及重連機制
websocket心跳重連(避免斷網、服務器重啓等)

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