在上篇中,我們探討了使用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連接,減少資源的浪費。如果想減輕服務器的壓力,心跳間隔可以根據需求增大。
參考文獻: