什么是websocket?
WebSocket 協議在2008年誕生,2011年成為國際標准。所有瀏覽器都已經支持了。
它的最大特點就是,服務器可以主動向客戶端推送信息,客戶端也可以主動向服務器發送信息,是真正的雙向平等對話,屬於服務器推送技術的一種。
其他特點包括:
(1)建立在 TCP 協議之上,服務器端的實現比較容易。
(2)與 HTTP 協議有着良好的兼容性。默認端口也是80和443,並且握手階段采用 HTTP 協議,因此握手時不容易屏蔽,能通過各種 HTTP 代理服務器。
(3)數據格式比較輕量,性能開銷小,通信高效。
(4)可以發送文本,也可以發送二進制數據。
(5)沒有同源限制,客戶端可以與任意服務器通信。
(6)協議標識符是ws
(如果加密,則為wss
),服務器網址就是 URL。
一、何為心跳?
心跳就是客戶端定時的給服務端發送消息,證明客戶端是在線的, 如果超過一定的時間沒有發送則就是離線了。
二、如何判斷在線離線?
當客戶端第一次發送請求至服務端時會攜帶唯一標識、以及時間戳,服務端到db或者緩存去查詢改請求的唯一標識,如果不存在就存入db或者緩存中,
第二次客戶端定時再次發送請求依舊攜帶唯一標識、以及時間戳,服務端到db或者緩存去查詢改請求的唯一標識,如果存在就把上次的時間戳拿取出來,使用當前時間戳減去上次的時間,
得出的毫秒秒數判斷是否大於指定的時間,若小於的話就是在線,否則就是離線;
三、若服務端宕機了,客戶端怎么做、服務端再次上線時怎么做?
客戶端則需要斷開連接,通過onclose 關閉連接,服務端再次上線時則需要清除之間存的數據,若不清除 則會造成只要請求到服務端的都會被視為離線。
四、使用springboot整合websocket
添加Pom依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> websocket的配置類: @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } } websocket的API @ServerEndpoint("/websocket/{sid}") @Component @Slf4j public class WebSocketServer { //靜態變量,用來記錄當前在線連接數。應該把它設計成線程安全的。 private static int onlineCount = 0; //concurrent包的線程安全Set,用來存放每個客戶端對應的MyWebSocket對象。 private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>(); //與某個客戶端的連接會話,需要通過它來給客戶端發送數據 private Session session; //接收sid private String sid=""; /** * 連接建立成功調用的方法*/ @OnOpen public void onOpen(Session session,@PathParam("sid") String sid) { this.session = session; webSocketSet.add(this); //加入set中 addOnlineCount(); //在線數加1 log.info("有新窗口開始監聽:"+sid+",當前在線人數為" + getOnlineCount()); this.sid=sid; try { sendMessage("連接成功"); } catch (IOException e) { log.error("websocket IO異常"); } } /** * 連接關閉調用的方法 */ @OnClose public void onClose() { webSocketSet.remove(this); //從set中刪除 subOnlineCount(); //在線數減1 log.info("有一連接關閉!當前在線人數為" + getOnlineCount()); } @Autowired private RedisUtil<Object> redisUtil = (RedisUtil<Object>) SpringUtil.getBean("RedisUtil"); /** * 收到客戶端消息后調用的方法 * * @param message 客戶端發送過來的消息*/ @OnMessage public void onMessage(String message, Session session) { //清空表 log.info("收到來自窗口"+sid+"的信息:"+message); //clinet數據 DeviceInfo Deviceinfo=FastJsonUtils.toBean(message, DeviceInfo.class); Deviceinfo.setBeatTime(System.currentTimeMillis()); log.info(Deviceinfo.toString()); String jsonStr=redisUtil.get(Deviceinfo.getDeviceId()); if(null==jsonStr) { redisUtil.set(Deviceinfo.getDeviceId(), Deviceinfo); }else { //服務端數據 DeviceInfo redisData= FastJsonUtils.toBean(redisUtil.get(Deviceinfo.getDeviceId()),DeviceInfo.class); Long redistime= redisData.getBeatTime(); //當前系統時間 Long dates= System.currentTimeMillis(); Long time= dates-redistime; log.info("endTime="+dates+"startTime="+redistime+"總時長="+time); if(time<=15*1000) { log.info(Deviceinfo.getDeviceId()+"在線"); }else { log.info(Deviceinfo.getDeviceId()+"離線"); } redisUtil.set(Deviceinfo.getDeviceId(), Deviceinfo); } //群發消息 for (WebSocketServer item : webSocketSet) { try { item.sendMessage(message); // item.sendInfo(message,sid); } catch (IOException e) { e.printStackTrace(); } } } /** * * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { log.error("發生錯誤"); error.printStackTrace(); } /** * 實現服務器主動推送 */ public void sendMessage(String message) throws IOException { this.session.getBasicRemote().sendText(message); } /** * 群發自定義消息 * */ public static void sendInfo(String message,@PathParam("sid") String sid) throws IOException { log.info("推送消息到窗口"+sid+",推送內容:"+message); for (WebSocketServer item : webSocketSet) { try { //這里可以設定只推送給這個sid的,為null則全部推送 if(sid==null) { item.sendMessage(message); }else if(item.sid.equals(sid)){ item.sendMessage("server:"+message); } } catch (IOException e) { continue; } } } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { WebSocketServer.onlineCount++; } public static synchronized void subOnlineCount() { WebSocketServer.onlineCount--; } }
HTML頁面
<!DOCTYPE HTML>
<html>
<head>
<title>Test My WebSocket</title>
<meta charset="utf-8">
<script type="text/javascript" src="../js/jquery.min.js"></script>
</head>
<body>
TestWebSocket
<input id="texts" value="" type="text" />
<button onclick="send()">SEND MESSAGE</button>
<button οnclick="closeWebSocket()">CLOSE</button>
<div id="message"></div>
<script type="text/javascript">
var lockReconnect = false;//避免重復連接
var wsUrl = "ws://localhost:8889/websocket/001";
var ws;
var tt;
function createWebSocket() {
try {
ws = new WebSocket(wsUrl);
init();
} catch(e) {
console.log('catch');
reconnect(wsUrl);
}
}
function init() {
ws.onclose = function () {
console.log('鏈接關閉');
reconnect(wsUrl);
};
ws.onerror = function() {
console.log('發生異常了');
reconnect(wsUrl);
};
ws.onopen = function () {
//心跳檢測重置
heartCheck.start();
};
ws.onmessage = function (event) {
//拿到任何消息都說明當前連接是正常的
console.log('接收到消息');
heartCheck.start();
}
}
function reconnect(url) {
if(lockReconnect) {
return;
};
lockReconnect = true;
//沒連接上會一直重連,設置延遲避免請求過多
tt && clearTimeout(tt);
tt = setTimeout(function () {
createWebSocket(url);
lockReconnect = false;
}, 4000);
}
//心跳檢測
var heartCheck = {
timeout: 10000,
timeoutObj: null,
serverTimeoutObj: null,
start: function(){
console.log('start');
var self = this;
this.timeoutObj && clearTimeout(this.timeoutObj);
this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
this.timeoutObj = setTimeout(function(){
//這里發送一個心跳,后端收到后,返回一個心跳消息,
var timestamp = (new Date()).getTime();
//,"beatTime":timestamp
var obj={"deviceId":"001","userId":"119"};
var a = JSON.stringify(obj);
ws.send(a);
self.serverTimeoutObj = setTimeout(function() {
console.log(ws);
ws.close();
}, self.timeout);
}, this.timeout)
}
}
createWebSocket(wsUrl);
</script>
</body>
</html>