您现在的位置是:首页 >技术杂谈 >WebSocket详解-02网站首页技术杂谈
WebSocket详解-02
一、背景
Websocket是html5提出的一个协议规范,是为解决客户端与服务端实时通信。
WebSocket在连接关闭时会触发onclose事件, 在连接异常时会触发onerror事件。但在弱网环境下, 它们的触发灵敏度不高, 往往延迟很久才触发, 前端再去进行重连操作, 这样很不友好。
本文将介绍使用心跳重连机制来改善这一问题。
二、原理
心跳重连机制:前端在WS连接成功的情况下,开始执行心跳函数,首先向服务器端发送ping信息, 服务器内若收到信息则会返回pong信息。在一定时间内, 前端收到服务器返回的信息, 则表示此连接是正常的, 便重置心跳函数; 若前端在一定时间内没有收到心跳函数, 则表明没有连接成功, 此时前端关闭ws, 再执行重连操作。
三、代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
#container {
display: grid;
/*三列, 每列宽50px*/
grid-template-columns: repeat(4, 50px);
/*三行, 每行高50px*/
grid-template-rows: 50px 50px 50px;
margin-top: 20px;
}
.item {
border: 1px solid #e5e4e9;
cursor: pointer;
font-size: 12px;
color: #ffffff;
display: flex;
justify-content: center;
align-items: center;
}
.item-1 {
background-color: #ef342a;
}
</style>
</head>
<body>
<h1>Websocket心跳检测</h1>
<div id="container">
<div id="btn" class="item item-1">手动停止</div>
</div>
<script>
class Socket {
constructor(options) {
// Websocket地址
this.url = options.url;
// 监听服务器返回数据
this.handleMessage = options.handleMessage;
// Websocket实例对象
this.ws = null;
// 心跳函数定时器1:定时发送指令
this.pingTimer = null;
// 心跳函数定时器2:定时关闭重连
this.serverTimer = null;
this.timeout = options.timeout;
}
/**
* 建立连接
*/
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = (e) => {
console.log("连接成功", e);
// 启动心跳检测
this.handleHeartCheck();
};
this.ws.onmessage = (e) => {
this.handleMessage(e.data);
// 接收到消息则重置心跳函数
this.handleHeartCheck();
};
this.ws.onclose = (e) => {
console.log("连接关闭", e);
this.handleClose(e);
};
this.ws.onerror = (e) => {
console.log("连接报错", e);
this.handleClose(e);
};
}
/**
* 心跳检查
*/
handleHeartCheck() {
if (this.pingTimer) {
clearTimeout(this.pingTimer);
this.pingTimer = null;
}
if (this.serverTimer) {
clearTimeout(this.serverTimer);
this.serverTimer = null;
}
this.pingTimer = setTimeout(() => {
// 发送探测指令
this.ws.send(JSON.stringify({ type: "ping" }));
// 超过一定时间还没响应, 手动关闭,再重连
this.serverTimer = setTimeout(() => {
console.log("断线重连");
this.ws.close();
}, this.timeout);
}, this.timeout);
}
/**
* 关闭webSocket
*/
handleClose(e) {
if (this.pingTimer) {
clearTimeout(this.pingTimer);
this.pingTimer = null;
}
if (this.serverTimer) {
clearTimeout(this.serverTimer);
this.serverTimer = null;
}
this.connect();
}
}
</script>
<script>
// 初始化
const ws = new Socket({
url: "ws://localhost:8765",
timeout: 3000, // 心跳检测时间
handleMessage: function (data) {
// 监听服务器返回信息
console.log("服务器返回信息", data);
},
});
ws.connect();
document.getElementById("btn").onclick = () => {
console.log("手动关闭");
ws.ws.close();
};
</script>
</body>
</html>
四、Stomp介绍
概念:面向消息的简单文本协议
websocket定义了两种传输信息类型:文本信息和二进制信息。类型虽然被确定,但是他们的传输体是没有规定的。所以,需要用一种简单的文本传输类型来规定传输内容,它可以作为通讯中的文本传输协议。
STOMP是基于帧的协议,客户端和服务器使用STOMP帧流通讯。
一个STOMP客户端是一个可以以两种模式运行的用户代理,可能是同时运行两种模式。
作为生产者,通过SEND框架将消息发送给服务器的某个服务。
作为消费者,通过SUBSCRIBE制定一个目标服务,通过MESSAGE框架,从服务器接收消息。
五、Stomp常用代码
// 发送消息
stompClient.send('/topic/terminal_chart', {}, "ping")
// 订阅(subscription是一个对象,包含一个id属性,对应这个这个客户端的订阅ID)
var subscription = client.subscribe("/queue/test", callback);
// 终止订阅消息
subscription.unsubscribe();
// 获取STOMP子协议的客户端对象
stompClient = Stomp.over(socket);
// 心跳发送频率
stompClient.heartbeat.outgoing = 20000;
// 心跳接收频率
stompClient.heartbeat.incoming = 20000;
// 调用.connect方法连接Stomp服务端进行验证
stompClient.connect({}, ()=>{
// 成功回调
}, ()=>{
// 失败回调(第一次连接失败和连接后断开连接都会调用这个函数)
})
// 判断是否连接
stompClient.connected
六、Stomp自带心跳机制
stomp默认的心跳为10000ms,
heartbeat.outgoing:客户端发给服务端的心跳,* 0表示它不能发送心跳 * 否则它是能保证两次心跳的最小毫秒数
heartbeat.incoming:客户端希望服务端发送的心跳。* 0表示它不想接收心跳 * 否则它表示两次心跳期望的毫秒数
CONNECT
heart-beat:<cx>,<cy> 客户端
CONNECTED:
heart-beat:<sx>,<sy> 服务端
对于client发送server的心跳: * 如果<cx>为0(client不能发送心跳)或者<sy>为0(server不想接收心跳),将不起任何作用。心跳频率为MAX(<cx>,<sy>)毫秒数.
对于server发送client的心跳:心跳频率为MAX(<cy>,<sx>)毫秒数.
七、Stomp手写心跳检测
如果服务端禁止接收心跳,则stomp自带心跳机制将不起作用,如下手写的代替。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="./stomp.js"></script>
</head>
<body>
<script>
class Socket {
constructor(options) {
// 地址
this.url = options.url;
// 目的地
this.target = options.target;
// 客户端对象
this.stompClient = null;
// 心跳频率
this.timeout = options.timeout;
// 定时发送心跳
this.pingTimeout = null;
// 定时重连
this.serverTimer = null;
// 服务端返回的数据处理
this.handleMessage = options.handleMessage;
}
connect() {
let websocket = new WebSocket(this.url);
this.stompClient = Stomp.over(websocket);
this.stompClient.connect(
{},
this.connectCallback,
this.errorCallback
);
}
// 成功回调
connectCallback = (e) => {
console.log("连接成功", e);
this.handleHeartBeat();
this.stompClient.subscribe(this.target, (res) => {
if (res.body === "ping") {
console.log("心跳返回数据", res);
this.handleHeartBeat();
} else {
// 其他逻辑
this.handleMessage(res);
}
});
};
// 失败回调(连接失败时的回调函数,此函数重新调用连接方法,形成循环,直到连接成功)
errorCallback = () => {
console.log("失败重连");
if (this.pingTimeout) {
clearInterval(this.pingTimeout);
this.pingTimeout = null;
}
if (this.serverTimer) {
clearInterval(this.serverTimer);
this.serverTimer = null;
}
this.connect();
};
handleHeartBeat() {
if (this.pingTimeout) {
clearInterval(this.pingTimeout);
this.pingTimeout = null;
}
if (this.serverTimer) {
clearInterval(this.serverTimer);
this.serverTimer = null;
}
this.pingTimeout = setTimeout(() => {
// 发出探测指令
this.stompClient.send(this.target, {}, "ping");
this.serverTimer = setTimeout(() => {
this.errorCallback();
}, this.timeout);
}, this.timeout);
}
}
</script>
<script>
let ws = new Socket({
url: "ws://10.100.31.48:1995/customer/bj-metro-server-customer",
target: "/topic/terminal_chart",
timeout: 10000,
// 订阅信息的返回
handleMessage: (res) => {},
});
ws.connect();
</script>
</body>
</html>