The reconnecting-websocket library is the same as WebSocketapi
, but can WS
automatically reconnect when disconnected. The version based on this article will reconnecting-websocket 1.0.1
explain its usage and implementation principles in detail. Friends who are familiar with Leng Hammer know that, without much to say, go straight to the dry goods! ! !
basic use
Next, we will create a Node
side WS
service, and then create a client file to initialize WebSocket
the connection to our WS
service, and then demonstrate WS
the operation of automatic reconnection recovery after disconnection.
- Create a
WS
server service
The terminal installation nodejs-websocket
library helps us create the WS service on the node side:
# 安装 nodejs-websocket 库
npm i nodejs-websocket -S
复制代码
Create node
script index.js
:
const NodeJsWebsocket = require('nodejs-websocket');
var server = NodeJsWebsocket.createServer(function (conn) {
console.log('find a new connection.')
conn.on('error', (error) => {
console.error('[AssistDesign] connect error: ', error);
});
});
server.listen(3000, () => {
console.log('[server] server is running at port 3000');
});
复制代码
By node index.js
starting our node
service, you can see the terminal output [server] server is running at port 3000
to indicate that our service started successfully.
- Create a client connection
Create a new index.html
file by CDN
importing reconnecting-websocket.js
the library and initializing WS
the connection:
<!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>
</head>
<body>
<script src="https://cdn.bootcdn.net/ajax/libs/reconnecting-websocket/1.0.0/reconnecting-websocket.js"></script>
<script>
const cancel = document.querySelector('#cancel');
const url = `ws://localhost:3000`;
const ws = new ReconnectingWebSocket(url, null, {
reconnectInterval: 3000,
});
ws.onopen = function() {
console.log('connect successful', Date.now())
}
ws.onclose = function() {
console.log('connect close', Date.now());
}
cancel.addEventListener('click', () => {
console.log('click close', Date.now())
ws.close();
})
</script>
</body>
</html>
复制代码
After we have the files on the client side index.html
, we can access our files by starting a hot update service locally through tools such as live-server
or , and then we can put the computer to sleep to see the effect of unexpected disconnection and reconnection.http-server
index.html
WS
After learning how to use it, let's start analyzing the source code! ! ! Get ready, source code is always a headache, but the most rewarding! ! ! Forgive me for washing everyone's head again! ! ! However, before analyzing the source code in detail, let's review the knowledge points of custom events.
补充知识:自定义事件
自定义事件想必是大家都听过学过但是应该很少使用到实战的吧,相信本文也可以给大家对自定义事件的实战场景打开一个思路。
EventTarget.dispatchEvent(event)
向一个指定的事件目标派发一个事件,并以合适的顺序同步调用目标元素相关的事件处理函数。参数event
。具体可看EventTarget.dispatchEvent文档、Event文档,看下面个例子:
// 监听自定义事件
window.addEventListener('custom-event', function() {
console.log('custom event emitted');
}, false);
/**
* 创建一个新的事件对象
* @see https://developer.mozilla.org/zh-CN/docs/Web/API/Event/Event
*/
const event = new Event('custom-event', {
bubbles: true, // 冒泡
cancelable: false, // 不可取消
});
// 触发自定义事件
window.dispatchEvent(event);
复制代码
除了windows
可以触发自定义事件,触发自定义事件的目标也可以是其他元素,如下演示div
触发自定义事件的例子:
const div = document.createElement('div');
div.addEventListener('dom-custom-event', function() {
console.log('dom custom event emitted');
}, false);
const event2 = new Event('dom-custom-event', {
bubbles: true, // 冒泡
cancelable: false, // 不可取消
});
div.dispatchEvent(event2);
复制代码
值得注意的是Event
不兼容IE,替代方案是使用document.createEvent
:
/**
* 创建自定义事件
* document.createEvent作用是创建指定类型的事件
* @see https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createEvent
*/
const ieEvent = document.createEvent('CustomEvent');
// 创建一个新的事件对象
ieEvent.initCustomEvent('ie-custom-event', false, false, {
// other your custom params
myParam: 'this is data',
});
// 触发自定义事件
// 控制台输出 ie-custom-event emitted: this is data
window.dispatchEvent(ieEvent);
复制代码
源码实现
打开源码目录可以看到,目录结构很简单,源码实现都在reconnecting-websocket.js
中,大约三百行左右,如下图所示:
打开reconnecting-websocket.js
文件,整体就是对外暴露的一个立即执行函数,这个立即执行函数,大家在工程化流行起来之前应该会经常看到的:
(function (global, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof module !== 'undefined' && module.exports){
module.exports = factory();
} else {
global.ReconnectingWebSocket = factory();
}
})(this, function () {
// 要求浏览器环境要支持WebSocket对象
if (!('WebSocket' in window)) {
return;
}
function ReconnectingWebSocket(url, protocols, options) {
// ....
// 是否在实例化时就直接进行ws连接
if (this.automaticOpen == true) {
this.open(false);
}
// ....
}
return ReconnectingWebSocket;
});
复制代码
这个立即执行函数很熟悉吧,就是让该库同时支持CMD、AMD
等环境,对于这块不熟悉的可以学习如何封装一个插件模块。然后创建了ReconnectingWebSocket
类,该类有着和WebSocket
相同的API
实现,调用该类的时候用户参数决定是否直接调用open实例方法自动连接WS
服务。下面我们看看ReconnectingWebSocket
内部的实现:
// 默认参数
var settings = {
/** Whether this instance should log debug messages. */
debug: false,
/** Whether or not the websocket should attempt to connect immediately upon instantiation. */
automaticOpen: true,
/** The number of milliseconds to delay before attempting to reconnect. */
reconnectInterval: 1000,
/** The maximum number of milliseconds to delay a reconnection attempt. */
maxReconnectInterval: 30000,
/** The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. */
reconnectDecay: 1.5,
/** The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. */
timeoutInterval: 2000,
/** The maximum number of reconnection attempts to make. Unlimited if null. */
maxReconnectAttempts: null,
/** The binary type, possible values 'blob' or 'arraybuffer', default 'blob'. */
binaryType: 'blob'
}
if (!options) {
options = {};
}
// Overwrite and define settings with options if they exist.
// 参数合并
// TODO:该库把这些参数都合并到this上了,也就是合并到了原型对象上
for (var key in settings) {
if (typeof options[key] !== 'undefined') {
this[key] = options[key];
} else {
this[key] = settings[key];
}
}
// 下面这些都是只读属性
/** url连接地址 */
this.url = url;
/** 尝试重连的次数,或上次连接成功的次数 */
this.reconnectAttempts = 0;
/**
* The current state of the connection.
* Can be one of: WebSocket.CONNECTING, WebSocket.OPEN, WebSocket.CLOSING, WebSocket.CLOSED
* Read only.
*/
this.readyState = WebSocket.CONNECTING;
/**
* A string indicating the name of the sub-protocol the server selected; this will be one of
* the strings specified in the protocols parameter when creating the WebSocket object.
* Read only.
*/
this.protocol = null;
复制代码
这里主要是定义一些默认参数,比如断开重连的时间间隔、重连次数、最大重连时间间隔等。接下来就是创建自定义事件了,这部分是该库的核心实现,主要是通过自定义事件来代理触发ws相关事件的回调等,代码实现如下:
// 缓存this
var self = this;
var ws;
var forcedClose = false;
// 是否连接超时了
var timedOut = false;
// 创建一个div,用于自定义事件的dom载体
var eventTarget = document.createElement('div');
// 监听open、close、message等自定义事件,
// 响应的处理函数对应指定为实例对象上的onopen、onclose、onmessage等方法
eventTarget.addEventListener('open', function(event) {
self.onopen(event);
});
eventTarget.addEventListener('close', function(event) {
self.onclose(event);
});
eventTarget.addEventListener('connecting', function(event) {
self.onconnecting(event);
});
eventTarget.addEventListener('message', function(event) {
self.onmessage(event);
});
eventTarget.addEventListener('error', function(event) {
self.onerror(event);
});
复制代码
注意了,需要打起精神了,上面的代码非常重要!!! 非常重要!!!非常重要!!!这里是先通过var eventTarget = document.createElement('div');
创建一个div
元素,然后侦听了该div
元素上的open | connecting | message | error
等事件,其实也就是给该div
元素添加了自定义事件,事件的回调都是调用self.xxxx
方法,也就是调用的ReconnectingWebSocket
类上的实例方法,如果用户有添加自定义事件则使用用户设置的自定义事件回调,比如用户在使用时的代码如下:
const ws = new ReconnectingWebSocket(url, null, {
reconnectInterval: 3000,
});
// 用户使用该库时手动指定了onopen回调函数,则使用用户的这个回调函数
ws.onopen = function() {
console.log('connect successful', Date.now())
}
复制代码
如果用户没有指定回调函数呢,那么默认的回调函数如下,都是一个空函数而已,就是占个位,防止调用的时候报错:
// 设置ws相关事件监听函数的默认值,为空函数
ReconnectingWebSocket.prototype.onopen = function (event) { };
/** WS连接状态变为CLOSED时触发的默认钩子函数*/
ReconnectingWebSocket.prototype.onclose = function (event) { };
/** WS开始连接时触发的默认钩子函数 */
ReconnectingWebSocket.prototype.onconnecting = function (event) { };
/** WS接收消息时触发的默认钩子函数 */
ReconnectingWebSocket.prototype.onmessage = function (event) { };
/** WS连接出错时默认的钩子函数 */
ReconnectingWebSocket.prototype.onerror = function (event) { };
复制代码
继续往下面看,这里通过div
元素的自定义事件来代理调用ReconnectingWebSocket
类上的方法,从而达到了on*
开头的事件和WebSocket
保持了一致。因为要实现和WebSocket
相同的API,那如果要是ReconnectingWebSocket
实例通过addEventListener
方式的侦听事件回调该怎么处理呢?
/**
* 将实例对象上的addEventListener等监听函数绑定为eventTarget对象上的监听函数,
* 这样用户在使用addEventListener时实际上创建的是eventTarget的监听事件,
* 因此eventTarget.dispatchEvent分发事件时才可以触发用户设置的监听事件
*/
this.addEventListener = eventTarget.addEventListener.bind(eventTarget);
this.removeEventListener = eventTarget.removeEventListener.bind(eventTarget);
this.dispatchEvent = eventTarget.dispatchEvent.bind(eventTarget);
复制代码
这里库的做法是将ReconnectingWebSocket
实例的addEventListener
等方法代理到eventTarget
对象上,也就是代理到刚才到div
元素上,这样实际上也就是eventTarget.addEventListener
在侦听事件,而不是ReconnectingWebSocket
实例在侦听事件。
小小总结一下,为了模拟WebSocket
的API
,实际上将ReconnectingWebSocket
实例上所有的侦听函数都代理到了的eventTarget
对象上,也就是代理到了创建的这个div
元素上。这样,我们才能在后续的真正的ws相关逻辑内通过该div
元素来触发自定义事件,因为前面都是监听用户设置的各种事件。
下面我们来看是如何实现WS
部分的。从一开始我们知道,ReconnectingWebSocket
类在实例的时候,先是初始化相关参数后,紧接着其实就是调用了这个this.open
方法。那这个open
方法他是做什么的呢?看下面代码:
/**
* 创建WS连接
* @param {boolean} reconnectAttempt 是否尝试重连
*/
this.open = function(reconnectAttempt) {
ws = new WebSocket(self.url, protocols || []);
ws.binaryType = this.binaryType;
if (reconnectAttempt) {
// 禁止重连次数大于用户设置的最大连接次数
if (this.maxReconnectAttempts
&& this.reconnectAttempts > this.maxReconnectAttempts
) {
return;
}
} else {
// 触发eventTarget的connecting自定义事件
// 此时用户的onconnecting钩子函数会被调用
eventTarget.dispatchEvent(generateEvent('connecting'));
this.reconnectAttempts = 0;
}
if (self.debug || ReconnectingWebSocket.debugAll) {
console.debug('ReconnectingWebSocket', 'attempt-connect', self.url);
}
var localWs = ws;
// 连接超时的处理逻辑
var timeout = setTimeout(function() {
if (self.debug || ReconnectingWebSocket.debugAll) {
console.debug('ReconnectingWebSocket', 'connection-timeout', self.url);
}
timedOut = true;
localWs.close();
timedOut = false;
}, self.timeoutInterval);
// ws连接成功的处理逻辑
ws.onopen = function(event) {
clearTimeout(timeout);
if (self.debug || ReconnectingWebSocket.debugAll) {
console.debug('ReconnectingWebSocket', 'onopen', self.url);
}
// 连接成功后更新相关状态
// 当前类是WebSocket的模拟实现,因此要把ws相关的属性更新到当前类上
self.protocol = ws.protocol;
self.readyState = WebSocket.OPEN;
self.reconnectAttempts = 0;
// 触发eventTarget的自定义open事件
// 因此用户设置onopen钩子会被触发
var e = generateEvent('open');
e.isReconnect = reconnectAttempt;
reconnectAttempt = false;
eventTarget.dispatchEvent(e);
};
// ws连接关闭的处理逻辑
ws.onclose = function(event) {
clearTimeout(timeout);
ws = null;
// 用户手动调用ws.close()时,不会触发重连操作
if (forcedClose) {
self.readyState = WebSocket.CLOSED;
eventTarget.dispatchEvent(generateEvent('close'));
} else {
self.readyState = WebSocket.CONNECTING;
// 触发eventTarget的自定义事件connecting
// 此时用户设置的onconnecting钩子函数会被触发
var e = generateEvent('connecting');
e.code = event.code;
e.reason = event.reason;
e.wasClean = event.wasClean;
eventTarget.dispatchEvent(e);
// 当不是重连操作且没有连接超时,才触发用户设置的onclose事件
// TODO: 此处存疑,为什么要判断连接没有超时
if (!reconnectAttempt && !timedOut) {
if (self.debug || ReconnectingWebSocket.debugAll) {
console.debug('ReconnectingWebSocket', 'onclose', self.url);
}
eventTarget.dispatchEvent(generateEvent('close'));
}
// 设置重连的时间,根据速率和连接次数依次变化, 例如第四次连接: 2000 * (1.5的四次方)
// 连接时间不会超过设置的最大重连时间,默认最大重连时间是30秒
var timeout = self.reconnectInterval * Math.pow(
self.reconnectDecay,
self.reconnectAttempts
);
setTimeout(function() {
self.reconnectAttempts++;
self.open(true);
},
timeout > self.maxReconnectInterval ? self.maxReconnectInterval: timeout);
}
};
// ws收到消息的处理逻辑
ws.onmessage = function(event) {
if (self.debug || ReconnectingWebSocket.debugAll) {
console.debug('ReconnectingWebSocket', 'onmessage', self.url, event.data);
}
// 触发用户设置的onmessage钩子函数
var e = generateEvent('message');
e.data = event.data;
eventTarget.dispatchEvent(e);
};
// ws出现错误的处理逻辑
ws.onerror = function(event) {
if (self.debug || ReconnectingWebSocket.debugAll) {
console.debug('ReconnectingWebSocket', 'onerror', self.url, event);
}
// 触发用户设置的onerror钩子函数
eventTarget.dispatchEvent(generateEvent('error'));
};
}
/**
* 创建自定义事件,
* TODO:这里不是要new Event的原因在于要兼容ie
*/
function generateEvent(s, args) {
var evt = document.createEvent("CustomEvent");
evt.initCustomEvent(s, false, false, args);
return evt;
};
复制代码
我们来分析下这个open
方法,可以看到open
方法内部才是真正的WS连接相关实现:
- 这里是先通过
ws = new WebSocket(self.url, protocols || []);
初始化一个WS连接,然后通过触发一个连接中的事件,即触发eventTarget
对象的connecting
事件来调用用户设置的connecting
回调函数。 - 如果超时了还没连接上,则关闭WS连接
ws
连接成功则把ws实例上的protocol
等属性复制到ReconnectingWebSocket
类实例上,同时触发eventTarget
对象的open
事件来调用用户设置的open
回调函数。ws
连接失败时则判断是否重新连接,不重连则触发close
事件回调,重连则是更新重连次数等,然后调用this.open
重新实现ws
连接逻辑,即重来了一次。ws
接收消息时则触发message
事件回调逻辑。ws
连接出错时则触发close
事件回调。
要注意的是generateEvent
函数的实现兼容了IE
环境。
处理完了相关事件的触发逻辑,那怎么发送消息的呢?看下面代码实现:
/**
* @param data a text string, ArrayBuffer or Blob to send to the server.
*/
this.send = function (data) {
if (ws) {
if (self.debug || ReconnectingWebSocket.debugAll) {
console.debug('ReconnectingWebSocket', 'send', self.url, data);
}
return ws.send(data);
} else {
throw 'INVALID_STATE_ERR : Pausing to reconnect websocket';
}
};
复制代码
发送逻辑比较简单,就是调用ws
实例的send
方法,简单包装了一下。同理主动关闭ws
逻辑,刷新的逻辑也是一样的简单包装:
/**
* Closes the WebSocket connection or connection attempt, if any.
* If the connection is already CLOSED, this method does nothing.
*/
this.close = function (code, reason) {
// Default CLOSE_NORMAL code
if (typeof code == 'undefined') {
code = 1000;
}
// 手动关闭的ws,不会在ws.onclose时触发自动重连逻辑
forcedClose = true;
if (ws) {
ws.close(code, reason);
}
};
/**
* 刷新ws服务
* 实现逻辑为:主动关闭ws,然后会在ws.onclose时触发重新连接
*/
this.refresh = function () {
if (ws) {
ws.close();
}
};
复制代码
到这里,源码分析的逻辑实现基本完了,但是大家知道的是,WebSocket本身有OPEN | CONNECTING | CLOSING | CLOSED
等属性,因为要实现和Websocket一样的API,所以这部分的静态属性也要拷贝过来:
/**
* 是否所有ReconnectingWebSocket实例都debug消息
*/
ReconnectingWebSocket.debugAll = false;
// 因为ReconnectingWebSocket要实现和ReconnectingWebSocket相同的api
// 所以拷贝WebSocket的静态属性到ReconnectingWebSocket上
ReconnectingWebSocket.CONNECTING = WebSocket.CONNECTING;
ReconnectingWebSocket.OPEN = WebSocket.OPEN;
ReconnectingWebSocket.CLOSING = WebSocket.CLOSING;
ReconnectingWebSocket.CLOSED = WebSocket.CLOSED;
return ReconnectingWebSocket;
复制代码
原理总结
该库websocket断开重连的核心逻辑如下图所示:
- 首先一个立即执行函数实现各环境模块支持
- 创建
ReconnectingWebSocket
类,该类和WebSocket
有着相同的语法实现 - 定义一些重连的基本参数,比如断开重连的时间间隔、重连次数、最大重连时间间隔等
- 创建自定义事件对象
eventTarget
,后续用户所有的ws.onopen
、ws.addEventListener('open', () => {}, false)
等钩子函数,都通过该事件对象触发。 - 自定义事件的实现考虑了IE的兼容
- 将
ReconnectingWebSocket
类实例上的addEventListener
函数代理为eventTarget
自定义事件模型上的同名函数,因此用户使用ws时设置的监听函数可以被如期触发。 - 实例化
Webscoket
:- 在
ws
连接成功时通过自定义事件触发open
事件 - 在
ws
主动关闭时,通过自定义事件触发close
事件。 - 在
ws
意外关闭时,进行重连操作。重连逻辑就是重新开始实例化Websocket
进行后续的逻辑。 - 在
ws
收到消息时通过自定义事件触发message
事件 - 在
ws
出错时通过自定义事件触发error事件
- 在
ReconnectingWebSocket
类实例上的open
、close
等方法,仅仅是对ws上的方法进行调用。- 把
Websocket
上的一些静态属性赋值给ReconnectingWebSocket
总结
The main point of this library is actually the logic implementation of custom events and WS
disconnection and reconnection. One is the idea of disconnecting and reconnecting, and the other is to ingeniously trigger the listener function set by the user in each hook of Websocket
the connection through the custom event , so that the library implements the same API logic as the one, which is not ingenious.WS
WebSocket