The Reconnecting -websocket library has the same API as Websocket, but can be automatically reconnected when WS disconnects. This article will explain its usage and implementation based on reconnecting-websocket 1.0.1. Those who are familiar with Leng Hammer know that they can go directly to dry goods without saying much!!

The basic use

Next we will create a WS service on the Node side, then initialize the WebSocket connection on our WS service by creating a client file, and then demonstrate the operation of automatic reconnection recovery after THE WS is disconnected.

  • To create aWSServer service

Installing the nodejs-webSocket library helps us create the WS service on the node side:

Install nodejs-websocket library
npm i nodejs-websocket -S
Copy the code

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');
});
Copy the code

[server] Server is running at port 3000 [server] Server is running at port 3000

  • Create a client connection

Create a new index.html file, import the reconnecting-websocket.js library via CDN and initialize the WS connection:

<! DOCTYPEhtml>
<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>
Copy the code

With the index. HTML file on the client side, we can access our index. HTML file by using a tool such as live-server or HTTP-server to enable a hot update service locally, and then we can put the computer to sleep to see the effect of WS accidental disconnection and reconnection.

After learning how to use, we will start the source code analysis ha!! Get ready, source code is always a headache, but the biggest benefit!! Forgive me for washing everyone’s head again!! However, before getting into the details of the source code, let’s review the knowledge of custom events.

Additional knowledge: Custom events

Custom events must be everyone has heard and learned but should rarely be used to combat it, I believe that this article can also give you a way to open the actual combat scene of custom events.

EventTarget. DispatchEvent (event) to a specified target distributing a events, and in the right order synchronous invocation target element related to the event handler. The parameters of the event. Specific to see EventTarget. DispatchEvent documents, the Event document, look at the following example:

// Listen for custom events
window.addEventListener('custom-event'.function() {
  console.log('custom event emitted');
}, false);

/** * Create a new event object@see https://developer.mozilla.org/zh-CN/docs/Web/API/Event/Event
 */
const event = new Event('custom-event', {
  bubbles: true./ / the bubbling
  cancelable: false.// Cannot be cancelled
});
// Triggers a custom event
window.dispatchEvent(event);
Copy the code

In addition to Windows, the target of a custom event can also be other elements. Here is an example of a div custom event:

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./ / the bubbling
  cancelable: false.// Cannot be cancelled
});
div.dispatchEvent(event2);
Copy the code

Note that Event is not compatible with IE. The alternative is to use document.createEvent:

Document. createEvent creates an event of the specified type@see https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createEvent
 */
const ieEvent = document.createEvent('CustomEvent');
// Create a new event object
ieEvent.initCustomEvent('ie-custom-event'.false.false, {
  // other your custom params
  myParam: 'this is data'});// Triggers a custom event
Emitted by the console ie-custom-event emitted by: this is data
window.dispatchEvent(ieEvent);
Copy the code

The source code to achieve

If you open the source directory, you can see that the structure of the directory is very simple, and the source code is implemented in reconnection-websocket.js, about 300 lines, as shown in the following figure:

Open the reconnection-websocket.js file, and the whole thing is exposed as an instant-execute function that you might have seen a lot before engineering became popular:

(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 () {
  // The browser environment is required to support WebSocket objects
  if(! ('WebSocket' in window)) {
    return;
  }
  
  function ReconnectingWebSocket(url, protocols, options) {
    / /...
    
    // Whether the WS connection is made directly at instantiation time
    if (this.automaticOpen == true) {
      this.open(false);
    }
    
    / /...
  }
  
  return ReconnectingWebSocket;
});
Copy the code

The execute now function is familiar enough to enable the library to support both CMD and AMD environments, but for those unfamiliar, you can learn how to package a plug-in module. Then we create the ReconnectingWebSocket class, which has the same API implementation as WebSocket. When calling this class, the user parameter determines whether to call the open instance method directly to automatically connect to the WS service. Let’s look at the internal implementation of ReconnectingWebSocket:

// Default parameters
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.
// Merge parameters
// TODO: The library incorporates these parameters into this, the prototype object
for (var key in settings) {
  if (typeofoptions[key] ! = ='undefined') {
    this[key] = options[key];
  } else {
    this[key] = settings[key]; }}// These are read-only attributes
/** url connection address */
this.url = url;

/** The number of reconnection attempts, or the number of successful connections */
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;
Copy the code

This section defines some default parameters, such as the interval for disconnection and reconnection, the number of reconnection times, and the maximum reconnection interval. Next is to create a custom event, this part is the core implementation of the library, mainly through the custom event to trigger the proxy WS related events callback, code implementation is as follows:

/ / this cache
var self = this;
var ws;
var forcedClose = false;
// Whether the connection timed out
var timedOut = false;
// Create a div for customizing the event's DOM carrier
var eventTarget = document.createElement('div');

// Listen for custom events like open, close, message, etc.
// The response handlers correspond to methods onopen, onclose, onMessage, etc., specified on the instance object
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);
});
Copy the code

Pay attention, need to brace up, the above code is very important!! Very important!! Very important!! Var eventTarget = document.createElement(‘div’); Create a div element, and then listen to the div elements on the open | connecting | message | events such as the error, but also is to give the div elements added custom events, event callback is called the self. The method of XXXX, If the user adds a custom event, the callback will use the custom event set by the user, such as the following code when the user uses it:

const ws = new ReconnectingWebSocket(url, null, {
  reconnectInterval: 3000});// If the user manually specified the onopen callback when using the library, the user's callback will be used
ws.onopen = function() {
  console.log('connect successful'.Date.now())
}
Copy the code

If the user does not specify a callback function, then the default callback function is as follows:

// Set the default value of the WS related event listener, which is empty
ReconnectingWebSocket.prototype.onopen = function (event) {};/** The default hook function that is triggered when the WS connection state becomes CLOSED
ReconnectingWebSocket.prototype.onclose = function (event) {};/** the default hook function that WS fires when it starts a connection */
 ReconnectingWebSocket.prototype.onconnecting = function (event) {};/** The default hook function that WS fires when receiving a message */
ReconnectingWebSocket.prototype.onmessage = function (event) {};/** the default hook function */ when the WS connection fails
ReconnectingWebSocket.prototype.onerror = function (event) {};Copy the code

Moving on, the ReconnectingWebSocket method is broached by a custom event on the div element, so events starting with on* are consistent with the WebSocket. Since we implement the same API as WebSocket, what if ReconnectingWebSocket instance listens for event callbacks via addEventListener?

/** * bind listener functions such as addEventListener on the instance object to listener functions on the eventTarget object, * so that the user actually creates listener events for the eventTarget when using addEventListener. * so eventTarget. DispatchEvent distributed event can trigger user Settings to monitor events * /
this.addEventListener = eventTarget.addEventListener.bind(eventTarget);
this.removeEventListener = eventTarget.removeEventListener.bind(eventTarget);
this.dispatchEvent = eventTarget.dispatchEvent.bind(eventTarget);
Copy the code

What the library does here is it proxies methods like addEventListener of the ReconnectingWebSocket instance onto the eventTarget object, that is, onto the div element, This in fact is the eventTarget. AddEventListener in listen for events, rather than ReconnectingWebSocket instance in listen for events.

To summarize, in order to simulate the WebSocket API, all listeners on the ReconnectingWebSocket instance are actually proxyed to the eventTarget object, that is, the div element that was created. This way, we can fire custom events through the div element in subsequent actual WS related logic, since we are listening for various user-set events.

Let’s see how the WS part is implemented. From the beginning, we know that the ReconnectingWebSocket class instantiates its parameters and then calls this. Open. So what does this open method do? Look at the following code:

/** * Create a WS connection *@param {boolean} ReconnectAttempt Whether to attempt to reconnect */
this.open = function(reconnectAttempt) {
  ws = new WebSocket(self.url, protocols || []);
  ws.binaryType = this.binaryType;

  if (reconnectAttempt) {
    // The number of reconnection attempts is greater than the maximum number set by the user
    if (this.maxReconnectAttempts
      && this.reconnectAttempts > this.maxReconnectAttempts
    ) {
      return; }}else {
    // Trigger the connecting event of the eventTarget
    // The user's onconnecting hook function is called
    eventTarget.dispatchEvent(generateEvent('connecting'));
    this.reconnectAttempts = 0;
  }

  if (self.debug || ReconnectingWebSocket.debugAll) {
    console.debug('ReconnectingWebSocket'.'attempt-connect', self.url);
  }

  var localWs = ws;
  // Connection timeout processing logic
  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 connection success processing logic
  ws.onopen = function(event) {
    clearTimeout(timeout);
    if (self.debug || ReconnectingWebSocket.debugAll) {
      console.debug('ReconnectingWebSocket'.'onopen', self.url);
    }
    // Update related status after successful connection
    // The current class is a mock implementation of WebSocket, so update the WS-related attributes to the current class
    self.protocol = ws.protocol;
    self.readyState = WebSocket.OPEN;
    self.reconnectAttempts = 0;

    // Trigger a custom open event for eventTarget
    // So the user setting onOpen hook is triggered
    var e = generateEvent('open');
    e.isReconnect = reconnectAttempt;
    reconnectAttempt = false;
    eventTarget.dispatchEvent(e);
  };

  // ws connection closure processing logic
  ws.onclose = function(event) {
    clearTimeout(timeout);
    ws = null;
    // When the user manually calls ws.close(), the reconnection operation is not triggered
    if (forcedClose) {
      self.readyState = WebSocket.CLOSED;
      eventTarget.dispatchEvent(generateEvent('close'));
    } else {
      self.readyState = WebSocket.CONNECTING;
      // Trigger the custom event of eventTarget
      // The onconnecting hook function set by the user is triggered
      var e = generateEvent('connecting');
      e.code = event.code;
      e.reason = event.reason;
      e.wasClean = event.wasClean;
      eventTarget.dispatchEvent(e);

      // The user-set onclose event is triggered only when there is no reconnection operation and no connection timeout
      // TODO:The connection did not time out
      if(! reconnectAttempt && ! timedOut) {if (self.debug || ReconnectingWebSocket.debugAll) {
          console.debug('ReconnectingWebSocket'.'onclose', self.url);
        }
        eventTarget.dispatchEvent(generateEvent('close'));
      }

      // Set the reconnection time according to the speed and number of connections, such as the fourth connection: 2000 * (1.5 ^ 4)
      // The connection time cannot exceed the set maximum reconnection time. The default maximum reconnection time is 30 seconds
      var timeout = self.reconnectInterval * Math.pow(
        self.reconnectDecay,
        self.reconnectAttempts
      );
      setTimeout(function() {
        self.reconnectAttempts++;
        self.open(true); }, timeout > self.maxReconnectInterval ? self.maxReconnectInterval: timeout); }};// ws receives the message processing logic
  ws.onmessage = function(event) {
    if (self.debug || ReconnectingWebSocket.debugAll) {
      console.debug('ReconnectingWebSocket'.'onmessage', self.url, event.data);
    }
    // Trigger the user-set onMessage hook function
    var e = generateEvent('message');
    e.data = event.data;
    eventTarget.dispatchEvent(e);
  };

  // ws failed to handle logic
  ws.onerror = function(event) {
    if (self.debug || ReconnectingWebSocket.debugAll) {
      console.debug('ReconnectingWebSocket'.'onerror', self.url, event);
    }
    // Trigger the user-set onError hook function
    eventTarget.dispatchEvent(generateEvent('error'));
  };
}

/** * create custom events, * TODO: This is not a new Event because it is compatible with IE */
function generateEvent(s, args) {
  var evt = document.createEvent("CustomEvent");
  evt.initCustomEvent(s, false.false, args);
  return evt;
};
Copy the code

Taking a look at the open method, we can see that inside the open method is the real WS connection-related implementation:

  • This is the first passws = new WebSocket(self.url, protocols || []);Initialize a WS connection and then fire by firing an event in a connectioneventTargetThe object’sconnectingEvent to call the user-setconnectingCallback function.
  • If the connection is not connected after the timeout, close the WS connection
  • wsIf the connection is successful, theprotocolAnd so onReconnectingWebSocketClass instanceeventTargetThe object’sopenEvent to call the user-setopenCallback function.
  • wsIf the connection fails, the system determines whether to reconnect. If the connection fails, the system triggers the reconnectioncloseEvent callback, reconnection is update reconnection times, etc., and then callthis.openTo achievewsJoin logic, that is, over again.
  • wsTriggered when a message is receivedmessageEvent callback logic.
  • wsTriggered when a connection error occurscloseEvent callback.

Note that the generateEvent function implementation is compatible with the IE environment.

After processing the triggering logic of the relevant event, how can the message be sent? Look at the following code implementation:

/ * * *@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'; }};Copy the code

The send logic is simple, simply wrapped up by calling the SEND method of the WS instance. In the same way that the WS logic is actively turned off, the refresh logic is the same simple wrapper:

/** * 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;
  }
  // Manually closed ws does not trigger automatic reconnection logic when ws. Onclose
  forcedClose = true;
  if(ws) { ws.close(code, reason); }};/** * Refresh the WS service * the implementation logic is: actively shut down WS, and then trigger reconnection when ws. Onclose */
this.refresh = function () {
  if(ws) { ws.close(); }};Copy the code

Finished here, source code analysis logic implementation of the basic, but as we all know that the WebSocket itself is OPEN | CONNECTING | CLOSING | CLOSED properties, such as for implementation and WebSocket API, so this part of the static attributes also will copy come over:

/** * Whether all ReconnectingWebSocket instances debug messages */
ReconnectingWebSocket.debugAll = false;

// Because ReconnectingWebSocket implements the same API as ReconnectingWebSocket
// Copy the static properties of WebSocket to ReconnectingWebSocket
ReconnectingWebSocket.CONNECTING = WebSocket.CONNECTING;
ReconnectingWebSocket.OPEN = WebSocket.OPEN;
ReconnectingWebSocket.CLOSING = WebSocket.CLOSING;
ReconnectingWebSocket.CLOSED = WebSocket.CLOSED;

return ReconnectingWebSocket;
Copy the code

The principle of summary

The core logic of websocket disconnection and reconnection is shown below:

  • First, an immediate execution function implements the support of each environment module
  • createReconnectingWebSocketClass, class andWebSocketIt has the same syntax implementation
  • Define some basic parameters of reconnection, such as disconnection interval, reconnection times, maximum reconnection interval, etc
  • Create custom event objectseventTarget, owned by subsequent usersws.onopen,ws.addEventListener('open', () => {}, false)And other hook functions are triggered by the event object.
  • The implementation of custom events takes into account IE compatibility
  • willReconnectingWebSocketClass instanceaddEventListenerThe function proxy iseventTargetCustom function of the same name on the event model, so that listener functions set by the user when using WS can be fired as expected.
  • instantiationWebscoket:
    • inwsTriggered by a custom event when the connection is successfulopenThe event
    • inwsActive shutdown is triggered by a custom eventcloseEvents.
    • inwsIn case of unexpected shutdown, reconnection is performed. Reconnecting logic is to restart the instantiationWebsocketDo the following logic.
    • inwsTriggers a custom event when a message is receivedmessageThe event
    • inwsTriggered by a custom event when an error occursError event
  • ReconnectingWebSocketClass instanceopen,closeAnd so on, just making calls to methods on WS.
  • theWebsocketSome static attributes assigned toReconnectingWebSocket

conclusion

The main focus of the library is actually custom events and the logical implementation of WS disconnection reconnection. The first is the idea of Websocket disconnect and reconnect, and the second is that the library implements the same API logic as Websocket by subtly triggering user-set listener functions in various hooks of WS connections through custom events.