Web Socket与HTTP一样,都是基于TCP的网络协议,二者最大的区别是Web Socket是全双工,HTTP是半双工。二者的其他特性网上有很多介绍,这里不再多说,本文主要介绍如何使用Web Socket,以及心跳检测和重连机制。
基本用法
前端部分:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>客户端</title>
</head>
<body>
这是一个客户端
<button id="btn">发送数据给服务器</button>
</body>
<script>
let btn=document.getElementById('btn');
//创建websocket对象
let ws=new WebSocket('ws://localhost:9595');//Web Socket是一种协议,因此应该用ws而不是http
//监听事件
//连接建立成功时
ws.addEventListener('open',e=>{
console.log('建立了ws连接');
console.log(e);
});
//收到服务器的信息时
ws.addEventListener('message',e=>{
console.log(e);
});
//连接关闭时
ws.addEventListener('close',e=>{
console.log('close',e);
});
//发生错误,连接无法存续时
ws.addEventListener('error',err=>{
console.log('err',err);
});
btn.addEventListener('click',function () {
//点击按钮发送数据给服务器
//数据是字符串、ArrayBuffer或Blob中的一种
ws.send('前端数据');
})
</script>
</html>
服务端(需要自己安装nodejs-websocket模块):
const ws=require('nodejs-websocket');
ws.createServer(connect=>{
console.log('创建了新的连接');
//客户端发送来的消息时
connect.on('text',data=>{
console.log('text',data);
//数据是字符串、ArrayBuffer或Blob中的一种
connect.send('服务端的数据');
});
//连接断开时
connect.on('close',e=>{
console.log('close',e);
});
//连接发生错误时
connect.on('error',e=>{
console.log('error',e);
})
}).listen(9595);//这个是端口号
在浏览器的开发者工具中可以查看具体的数据
心跳检测和重连机制
为什么要心跳检测?上面的这种写法,在正常情况下或许没有问题,如果服务器很久都没有响应呢,除非服务器关闭了连接,否则前端是不会触发error或者close事件的;同时,在客户端网络断开的情况下,前端也不会触发error或者close事件,这时候用户等了半天也不知道是自己的网络问题。
为了解决上面的问题,以前端作为主动方,定时发送ping消息,后端回复pong消息,用于检测网络和前后端连接问题。前端在一定时间内没有收到后端pong的消息,说明连接出现了异常,前端主动执行重连逻辑,直到重连成功或在重连一定次数后停止重连并告知用户网络异常。因为websocket链接必须是前端主动请求建立连接,因此重连肯定是给前端来做,所以判断重连逻辑都是写在前端。下面就来实现一下简单的心跳检测和重连。
先改造一下后端:
const ws=require('nodejs-websocket');
ws.createServer(connect=>{
console.log('创建了新的连接');
//客户端发送来的消息时
connect.on('text',data=>{
//如果是心跳检测消息时,直接回复pong
if(data==='ping'){
connect.send('pong');
}else {
console.log('正常的数据',data);
connect.send('服务端的数据');
}
});
//连接断开时
connect.on('close',e=>{
console.log('close',e);
});
//连接发生错误时
connect.on('error',e=>{
console.log('error',e);
})
}).listen(9595);
再改造前端:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>客户端</title>
</head>
<body>
这是一个客户端
<button id="btn">发送数据给服务器</button>
</body>
<script>
class MyWebSocket{
timeout=5000;//超时时间(这个时间内没心跳就算超时)
maxReconnectTimes=0;//重连的最大次数,0表示可以无限重连
reconnectTimes=0;//已经重连的次数
reconnectTime=5000;//每次重连的间隔时间
heartBeatTime=5000;//每次心跳的间隔时间
heartBeatTimer=null;//心跳定时器
timeoutTimer=null;//超时定时器
reconnectTimer=null;//每次重连的定时器
isReconnecting=false;//是否正在重连
isDone=false;//已经触发错误了
url='';//连接地址
socket=null;//websocket实例
constructor(options) {
this.url=options.url;
this.timeout=options.timeout?options.timeout:this.timeout;
this.maxReconnectTimes=options.maxReconnectTimes?options.maxReconnectTimes:this.maxReconnectTimes;
this.reconnectTimes=options.reconnectTimes?options.reconnectTimes:this.reconnectTimes;
this.reconnectTime=options.reconnectTime?options.reconnectTime:this.reconnectTime;
this.heartBeatTime=options.heartBeatTime?options.heartBeatTime:this.heartBeatTime;
this.errorFunc=options.errorFunc?options.errorFunc:this.errorFunc;
this.connect();
}
//连接操作,参数为是否重连操作
connect(){
this.socket=new WebSocket(this.url);
this.bindEvent();
}
//重连
reconnect(){
if(this.maxReconnectTimes&&this.reconnectTimes>=this.maxReconnectTimes){
if(this.isDone){
return;
}
//防止error和close触发触发两次错误回调
this.isDone=true;
//超过了重连次数,就执行错误回调不连了
this.errorFunc();
}else {
//已经在重连了(连接失败时error和close事件都会触发)
if(this.isReconnecting){
return;
}
//重新连接
this.isReconnecting=true;
this.reconnectTimer=setTimeout(()=>{
this.reconnectTimes++;
console.log(`第${this.reconnectTimes}次重连`);
this.connect();
//可以进行下一次重连了
this.isReconnecting=false;
},this.reconnectTime);
}
}
//绑定监听事件
bindEvent(){
let self=this;
//连接建立成功时
this.socket.onopen=function () {
console.log('建立了ws连接');
//在连接成功后启动定时器,开始心跳。前端发送一次ping,后端返回一次pong算一次心跳。
self.heartbeat();
}
//收到服务器的信息时
this.socket.onmessage=function (e) {
console.log(e);
//能收到消息就说明心跳没问题,可以开始下一次心跳
self.heartbeat();
}
//连接关闭时
this.socket.onclose=function (e) {
console.log('close',e);
self.reconnect();
}
//发生错误,连接无法存续时
this.socket.onerror=function (err) {
console.log('err',err);
//发生错误时需要重连
self.reconnect();
}
}
//重置心跳
resetHeartbeat(){
clearTimeout(this.heartBeatTimer);
clearTimeout(this.timeoutTimer);
}
//开始心跳
heartbeat(){
//有心跳就说明连接稳定,重置重连数据
this.reconnectTimes=0;
clearTimeout(this.reconnectTimer);
this.isReconnecting=false;
//每次心跳前先重置心跳
this.resetHeartbeat();
//heartBeatTime心跳一次
this.heartBeatTimer=setTimeout(()=>{
console.log('心跳');
this.socket.send('ping');
this.timeoutTimer=setTimeout(()=>{
//超过超时时间没有检测到心跳回复就主动关闭连接
//在调用close后,前端不会立即执行close事件回调,要等到下一次调用send方法时才会执行close事件的回调
//但是后端会在调用close后立即执行close事件回调
//try catch是无法捕获websocket的连接错误的,因此只能在close和error事件回调中执行重连操作
console.log('主动关闭连接');
this.socket.close();
//对于客户端断网的情况,浏览器是不会抛出错误的,在网络恢复后,浏览器会自动重新连接,并且不会触发close或者error事件
//这里手动调用reconnect的目的是在客户端断网的情况下,也能手动重连,在长时间网络断开的情况下可以提示用户,而不是一直等待网络恢复
this.reconnect();
},this.timeout);
},this.heartBeatTime)
}
//错误处理函数
errorFunc(){
throw new Error('网络错误');
}
}
let ws=new MyWebSocket({
url:'ws://localhost:9595',
maxReconnectTimes:5,//可以重连5次
});
let btn=document.getElementById('btn');
btn.addEventListener('click',function () {
//点击按钮发送数据给服务器
//数据是字符串、ArrayBuffer或Blob中的一种
ws.socket.send('前端数据');
})
</script>
</html>
参考文献
《JavaScript高级程序设计第四版》