什么是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依赖
org.springframework.boot
spring-boot-starter-websocket
websocket的配置类:
@Configurationpublic classWebSocketConfig {
@BeanpublicServerEndpointExporter serverEndpointExporter() {return newServerEndpointExporter();
}
}
websocket的API
@ServerEndpoint("/websocket/{sid}")
@Component
@Slf4jpublic classWebSocketServer {//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
private static CopyOnWriteArraySet webSocketSet = new CopyOnWriteArraySet();//与某个客户端的连接会话,需要通过它来给客户端发送数据
privateSession session;//接收sid
private String sid="";/*** 连接建立成功调用的方法*/@OnOpenpublic 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异常");
}
}/*** 连接关闭调用的方法*/@OnClosepublic voidonClose() {
webSocketSet.remove(this); //从set中删除
subOnlineCount(); //在线数减1
log.info("有一连接关闭!当前在线人数为" +getOnlineCount());
}
@Autowiredprivate RedisUtil redisUtil = (RedisUtil) SpringUtil.getBean("RedisUtil");/*** 收到客户端消息后调用的方法
*
*@parammessage 客户端发送过来的消息*/@OnMessagepublic voidonMessage(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();
}
}
}/***
*@paramsession
*@paramerror*/@OnErrorpublic voidonError(Session session, Throwable error) {
log.error("发生错误");
error.printStackTrace();
}/*** 实现服务器主动推送*/
public void sendMessage(String message) throwsIOException {this.session.getBasicRemote().sendText(message);
}/*** 群发自定义消息
**/
public static void sendInfo(String message,@PathParam("sid") String sid) throwsIOException {
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 intgetOnlineCount() {returnonlineCount;
}public static synchronized voidaddOnlineCount() {
WebSocketServer.onlineCount++;
}public static synchronized voidsubOnlineCount() {
WebSocketServer.onlineCount--;
}
}
HTML页面
Test My WebSocketTestWebSocket
SEND MESSAGE
CLOSE
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);