什么是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>
