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