在使用websocket的过程中,有时候会遇到网络断开的情况,但是在网络断开的时候服务器端并没有触发onclose的事件。这样会有:服务器会继续向客户端发送多余的链接,并且这些数据还会丢失。所以就需要一种机制来检测客户端和服务端是否处于正常的链接状态。因此就有了websocket的心跳了。还有心跳,说明还活着,没有心跳说明已经挂掉了。
1. 为什么叫心跳包呢?
它就像心跳一样每隔固定的时间发一次,来告诉服务器,我还活着。
2. 心跳机制是?
心跳机制是每隔一段时间会向服务器发送一个数据包,告诉服务器自己还活着,同时客户端会确认服务器端是否还活着,如果还活着的话,就会回传一个数据包给客户端来确定服务器端也还活着,否则的话,有可能是网络断开连接了。需要重连~
前台代码:
var websocket;
// 设置端口号
var serverPort = '8080';
// 设置路径
var url = '/platform-framework/api/osWebsocket';
// 获取IP地址
function getWebIP() {
var curIP = window.location.hostname;
return curIP;
}
function init(){
//ws地址
var wsuri = "ws://" + getWebIP() + ":" + serverPort + url;
try {
websocket = new WebSocket(wsuri);
} catch (e) {
console.log(' 您的浏览器暂时不支持 webSocket ');
}
websocket.onclose = function (e) {
websocket.close();
console.log("WebSocket 关闭");
}
websocket.onopen = function () {
//心跳检测重置
heartCheck.start();
}
//连接发生错误的回调方法
websocket.onerror = function () {
websocket.close();
console.log("WebSocket 连接发生错误");
}
}
// 初始化
init();
/***
* webSocket 自动接收数据
* 给外部预留接口,方便页面实时获取新内容
* 传递方法(messageCallback)进来,给方法赋返回值(websocket接收到的数据)
*/
function onMessage(messageCallback) {
websocket.onmessage = function (e) {
if (typeof(messageCallback) === 'function') {
messageCallback(JSON.parse(e.data));
}
// 心跳检测重置
heartCheck.reset();
}
}
// webSocket 发送消息方法
function sendSockTable(agentData) {
if (websocket.readyState === websocket.OPEN) {
//若是ws开启状态
websocketsend(agentData)
} else if (websocket.readyState === websocket.CONNECTING) {
// 若是 正在开启状态,则等待1s后重新调用
setTimeout(function () {
sendSockTable(agentData);
}, 1000);
} else {
// 若未开启 ,则等待1s后重新调用
setTimeout(function () {
sendSockTable(agentData);
}, 1000);
}
}
// 数据发送
function websocketsend(agentData) {
websocket.send(JSON.stringify(agentData));
}
// webSocket 关闭
function webSocketClose() {
websocket.close();
}
// webSocket 初始化
function webSocketInit() {
init();
}
// 关闭并重新链接
function webSocketRestart() {
websocket.close();
init();
}
// 强制关闭浏览器 调用websocket.close(),进行正常关闭
window.onunload = function() {
websocket.close();
}
// 心跳检测 超过60秒之后会判定后端主动断开了
var heartCheck = {
timeout: 60000,
timeoutObj: null,
reset: function(){
clearTimeout(this.timeoutObj);
this.start();
},
start: function(){
this.timeoutObj = setTimeout(function () {
console.info("客户端发送心跳:" + new Date());
websocket.send('心跳链接测试');
}, this.timeout)
}
}
export default { onMessage,sendSockTable, webSocketRestart, webSocketClose, webSocketInit}
具体的思路如下:
1. 第一步页面初始化,调用当前 js 时,自动调用 init()函数,目的是创建一个websocket的方法,并初始化websocket一些方法
2. 第二步调用 onMessage((response) = >{ }) 函数,获取webSocket自动推送消息;调用sendSockTable(数据)函数,发送消息,调用webSocketRestart()函数,重启webSocket
3. 如上,当网络断开的时候,会先调用onerror,onclose事件可以监听到,会进行相应操作。正常的情况下,是先调用onopen方法的,当接收到数据时,会被onmessage事件监听到。
4. 最后一步就是实现心跳检测的代码
后台代码:
package com.platform.api;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.websocket.EncodeException;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import org.apache.log4j.Logger;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
//import org.apache.log4j.Logger;
import com.alibaba.fastjson.JSONObject;
import com.github.pagehelper.util.StringUtil;
import com.platform.entity.OsOrderInfoVo;
import com.platform.entity.OsTableManagement;
import com.platform.entity.OsWebSocketVo;
import com.platform.service.OsOrderService;
import com.platform.service.OsTableManagementService;
import com.platform.util.ApplicationContextRegister;
import com.platform.util.ServerEncoder;
/**
* @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,
* 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
*/
@Component
@ServerEndpoint(value = "/api/websocket", encoders = { ServerEncoder.class })
public class OsWebSocketController {
private Logger log = Logger.getLogger(OsWebSocketController.class);
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
private static CopyOnWriteArraySet webSocketSet = new CopyOnWriteArraySet();
private static Map> map = new HashMap>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
/**
* 连接建立成功调用的方法a
* @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
* @throws IOException
*/
@OnOpen
public void onOpen(Session session) throws IOException{
this.session = session;
webSocketSet.add(this);
addOnlineCount();
log.info("OsWebSocketController -> 有新连接加入!当前在线人数为" + getOnlineCount());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(){
webSocketSet.remove(this);
subOnlineCount();
log.info("OsWebSocketController -> 有一连接关闭!当前在线人数为" + getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
* @param message 客户端发送过来的消息
* @param session 可选的参数
*/
@OnMessage
public void onMessage(String message, Session session) {
/***
* 心跳测试只是为了证明其连通性
* 因 前台传递websocket时,心跳测试和普通数据结构不同,所以在此判断,
* 可在其不是心跳测试时,做其他操作
*/
log.info("【WebSocket消息】接收心跳:{}"+ message);
if("心跳链接测试".equals(message)) {
} else {
// 可以添加
// OsWebSocketGoods vo = JSONObject.parseObject(message, OsWebSocketGoods.class);
}
//群发消息
for(OsWebSocketController item: webSocketSet){
try {
item.sendMessageTo(map);
} catch (IOException e) {
e.printStackTrace();
continue;
} catch (EncodeException e) {
e.printStackTrace();
continue;
}
}
}
/**
* 发生错误时调用
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error){
log.info("OsWebSocketController -> 发生错误!");
log.info("OsWebSocketController -> 错误信息:" + error.getMessage());
error.printStackTrace();
}
/**
* 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。
* @param message
* @throws IOException
*/
public void sendMessage(String message) throws IOException{
this.session.getBasicRemote().sendText(message);
}
public void sendMessageTo(Map> map) throws IOException, EncodeException {
this.session.getBasicRemote().sendObject(map);
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
OsWebSocketController.onlineCount++;
}
public static synchronized void subOnlineCount() {
OsWebSocketController.onlineCount--;
}
}
实现心跳检测的思路是:每隔一段固定的时间,向服务器端发送一个ping数据,如果在正常的情况下,服务器会返回一个pong给客户端,如果客户端通过onmessage事件能监听到的话,说明请求正常,这里我们使用了一个定时器,每隔3秒(时间自定义)的情况下,如果是网络断开的情况下,在指定的时间内服务器端并没有返回心跳响应消息,因此服务器端断开了,因此这个时候我们使用websocket.close关闭连接,在一段时间后(在不同的浏览器下,时间是不一样的,firefox响应更快),可以通过 onclose事件监听到。因此在onclose事件内,我们可以调用 reconnect事件进行重连操作。
js在手机熄屏后会中断,在唤醒之后js会继续执行。所以设置在js中的定时发送心跳包的功能在手机熄屏后就没法执行了。熄屏时间过长,这个时候链接就会被服务端强制断开,并且大部分手机在熄屏时,websocket连接就已经断开了。
解决办法: 使用H5提供的页面隐藏/显示API。
document.addEventListener('visibilitychange',function() {
if(document.visibilityState == 'hidden') {
//记录页面隐藏时间
let hiddenTime = new Date().getTime()
} else {
let visibleTime = new Date().getTime();
//页面再次可见的时间-隐藏时间>10S,重连
if((visibleTime - hiddenTime) / 1000 > 10){
// 主动关闭连接
WebSockets.webSocketClose();
// 1.5S后重连 因为断开需要时间,防止连接早已关闭了
setTimeout(function(){
WebSockets.openSocket()
},1500);
}else{
console.log('还没有到断开的时间')
}
}
}
此方法不仅适用熄屏后重连,也适用于手机浏览器被切换至后台运行时js中断的情况