Introduction

Ultra fast Websocket Server with rich API for creating real-time, complex applications.
The library is made for NodeJS and according to RFC6455 Standard for websocket version 13.
Very clean code with straightforward logic and no npm dependencies.

Features

  • RFC6455, websocket v.13
  • NodeJS v10+
  • no npm dependencies
  • small, medium and large messages (up to 9,007,199,254 gigabytes or ~9 quadrillion bytes)
  • internal HTTP server
  • socket (client) authentication
  • limit total number of the connected clients
  • limit the number of connected clients per IP
  • rooms (grouped websocket clients)
  • ping & pong supported
  • Regoch-Router integration (build complex, real-time API)
  • easy RxJS integration

Installation

Install the server in your project via npm:


  $ npm install --save regoch-websocket
  


Development

When developing the Regoch WS Server start the server with the command:


  ## start the test server
  $ nodemon examples/server/000dev.js
  

Exports

When npm is used there are several exported classes and libraries:


  const { RWServer, RWHttpServer, lib } = require('regoch-websocket');

  /*
    lib
    -------
    {
      subprotocol,
      raw,
      jsonRWS,
      websocket13,
      helper,
      getMessageSize,
      getMessageSizeFromBlob,
      StringExt
    }

   */
  

Quick Start

A short example which shows the power of the Regoch Websocket Server.

/**
 * An example with the built-in HTTP server.
 */
const { RWServer, RWHttpServer, lib } = require('regoch-websocket');
const helper = lib.helper;

const Router = require('regoch-router');
const router = new Router({debug: false});


// start internal HTTP server
const httpOpts = {
  port: 3211,
  timeout: 0 // if 0 the socket connection will never timeout
};
const rwHttpServer = new RWHttpServer(httpOpts);
const httpServer = rwHttpServer.start(); // nodeJS HTTP server instance
setTimeout(() => {
  // rwsHttpServer.restart();
  // rwsHttpServer.stop();
}, 3400);



// websocket ultra
const wsOpts = {
  timeout: 5*60*1000,
  maxConns: 5,
  maxIPConns: 3,
  storage: 'memory',
  subprotocol: 'jsonRWS',
  tightening: 100,
  version: 13,
  debug: false
};
const rws = new RWServer(wsOpts);
rws.socketStorage.init(null);
rws.bootup(httpServer);



/*** socket stream ***/
rws.on('connection', async socket => {
  /* send message back to the client */
  const msgWelcome = 'New connection from socketID ' + socket.extension.id;
  // socket.extension.sendSelf({id: helper.generateID(), from: 0, cmd: 'info', payload: msgWelcome});

  // rws.dataTransfer.send(msgWelcome, socket);

  /* authenticate the socket */
  const authkey = 'TRTmrt'; // can be fetched from the database, usually 'users' table
  socket.extension.authenticate(authkey); // authenticate the socket: compare authkey with the sent authkey in the client request URL ws://localhost:3211/something?authkey=TRTmrt

  helper.sleep(1300);

  /* socketStorage test */
  // await new Promise(resolve => setTimeout(resolve, 5500));
  // const socketFound = rws.socketStorage.findOne({ip: '::1'});
  // if (!!socketFound && socketFound.extension) console.log('found socket.extension::', socketFound.extension);

});




/*** message stream ***/
rws.on('message', (msg, msgSTR, msgBUF, socket) => {
  // console.log('\nreceived message SUBPROTOCOL::', msg); // after subprotocol
  console.log('\nreceived message STRING::', msgSTR); // after DataParser
  // console.log('\nreceived message BUFFER::', msgBUF); // incoming buffer
  // console.log('\nsocketID', socket.extension.id);
  // rws.dataTransfer.sendOne(msg, socket); // return message back to the sender
});



/*** route stream ***/
rws.on('route', (msgObj, socket, dataTransfer, socketStorage, eventEmitter) => { // msgObj:: {id, from, to, cmd, payload: {uri, body}}
  console.log('routeStream::', msgObj);
  const payload = msgObj.payload;

  // router transitional variable
  router.trx = {
    uri: payload.uri,
    body: payload.body,
    msgObj,
    socket,
    dataTransfer: rws.dataTransfer
  };


  // route definitions
  router.def('/shop/login', (trx) => { console.log('trx::', trx.uri); });
  router.def('/shop/product/:id', (trx) => { console.log('trx.uri::', trx.uri, '\ntrx.query::', trx.query, '\ntrx.params::', trx.params); });
  router.def('/send/me/back', (trx) => {
    const id = trx.msgObj.id;
    const from = 0;
    const to = trx.msgObj.from;
    const cmd = 'route';
    const payload = {uri: '/returned/back/21', body: {x: 'something', y: 28}};
    const msg = {id, from, to, cmd, payload};
    rws.dataTransfer.sendOne(msg, trx.socket);
  }); // send new route back to the client
  router.notfound((trx) => { console.log(`The URI not found: ${trx.uri}`); });

  // execute the router
  router.exe().catch(err => {
    console.log(err);
    rws.dataTransfer.sendOne({cmd: 'error', payload: err.stack}, socket);
  });

});
    

License

The software is published under MIT License.
The MIT License

Copyright (C) 2021  Saša Mikodanić http://www.regoch.org

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to
whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

RFC6455

The standard is described at the offical RFC6455 Standard page. Here we bring schemas which will help to understand the section 5.2. Data Framing what is essential in the Regoch Websocket Server

Basic postulates


1) The mask is defined in the first bit of the second byte. When messages are sent from the client mask = 1 and masking bytes exist. Masking bytes always have 4 bytes length. If the message is sent from the server to client there's no masking bytes and the mask = 0.
2) The payload length in the second byte is the most important for defining payload data length.
- if the payload length defined in the second byte is <126 then there's no extension to define payload length
- if the payload length defined in the second byte is =126   =0b1111110   =0x7E then there are 2 bytes (16 bits) extension to define payload length
- if the payload length defined in the second byte is =127   =0b1111111   =0x7F then there are 8 bytes (64 bits) extension to define payload length

· Sending messages from client to server

Small Messages (< 126 B), Medium Messages (= 126 B), Large Messages (= 127 B)

RFC6455 diagram 1

· Sending messages from server to client

Small Messages (< 126 B), Medium Messages (= 126 B), Large Messages (= 127 B)

RFC6455 diagram 2

class RWServer

The most important class which defines methods to start the websocket server and listen the events.

Properties

RWS

PropertyDescriptionTypeDefault
wsOptswebsocket server options (see the table below)object
serverHTTP server instanceobject
dataTransferdata transfer instanceobject
socketStoragesocket storage instanceobject


wsOpts

PropertyDescriptionTypeDefault
timeoutClose socket after ms inactivity. If 0 never close. Default: 5 minnumber5*60*1000
allowHalfOpenIf false close socket if it's readyState is writeOnly or readOnly,
for example when NginX timeout socket on the client side [Client -X- NginX --- WSServer(NodeJS)]
booleanfalse
maxConnsLimit connections to the servernumber10000
maxIPConnsLimit connections from the same IP address. If 0 then infinite.number1
storageSocket storage type: memory, redis, mongodb, ...stringmemory
subprotocolProtocol used internally in the server and client: jsonRWS, rawstringjsonRWS
tighteningDelays in the server process execution (miliseconds)number400
versionWebsocket versionnumber13
debugDebug incoming and outgoing messagesbooleanfalse

Methods

Use this methods to start the websocket server and listen its events.

  • constructor (wsOpts :object) :void

    Create the RWS class instance.

    
    const { RWServer } = require('regoch-websocket');
    const wsOpts = {
      timeout: 5*60*1000, // close the socket if the client didn't send any message in this period of miliseconds
      maxConns: 5,
      maxIPConns: 3,
      storage: 'memory',
      subprotocol: 'jsonRWS',
      tightening: 100,
      version: 13,
      debug: false
    };
    const rws = new RWServer(wsOpts);
          

  • bootup (httpServer :HTTPServer) :void

    Start the websocket server on the top of the HTTP server.
    httpServer - instance of the NodeJS HTTP Server

  • on (eventName :string, listener :Function) :void

    Listen the websocket events: 'connection', 'message', 'route'
    eventName - event name: 'connection', 'message', 'route'
    listener - callback function

Events

There are several events used in the client:
  1. connection - emitted when the client is succesfuly connected to the server
  2. message - emitted when client sent the valid message (valid means validated by the subprotocol)
  3. message-error - emitted when client sent invalid message (invalid means if the message doesn't conform the subprotocol rules)
  4. route - emitted when client sent the message with cmd: 'route' from the server. This is used to separate the route messages (messages useful for regoch-router) from other ordinary messages./li>
  • on ('connection', (socket :Socket) => { ... }) :void

    When client is connected to the websocket server.
    socket - net socket https://nodejs.org/api/net.html#net_class_net_socket

  • on ('message', (msg :any, msgSTR :string, msgBUF :Buffer, socket :Socket) => { ... }) :void

    When client sent the message to the websocket server.
    msg - the message after subprotocol is applied
    msgSTR - the message converted from buffer to string
    msgBUF - the message in buffer format (originally sent from the client, unmodified, with no parsing)
    socket - connected client, net socket https://nodejs.org/api/net.html#net_class_net_socket

  • on ('message-error', (err :Error) => { ... }) :void

    When server receive the message from the websocket client which is invalid (usually not valid in the subprotocol). Useful to catch the invalid messages sent from the client.
    err - error which caused invalid message

  • on ('route', (msg, socket, dataTransfer, socketStorage, eventEmitter) => { ... }) :void

    When client sent message with the route command. For example: {id: 210129163129492000, from: 210129163129492111, to: 0, cmd: 'route', payload: {uri: 'shop/login', body: {username:'mark', password:'thG5$#w'}}}
    msg - the message after subprotocol is applied
    socket - connected client, net socket https://nodejs.org/api/net.html#net_class_net_socket
    dataTransfer - DataTransfer instance
    socketStorage - SocketStorage instance
    eventEmitter - NodeJS EventEmitter instance

class RWHttpServer

The internal HTTP server. Simple creates a HTTP server instance on which websocket server will be attached.

Properties

httpOpts

PropertyDescriptionTypeDefault
portHTTP server port numbernumber3000
timeoutms of inactivity after ws will be closed. If 0 then the ws will never close. Default is 5 minutes.number5*60*1000

Methods

Use this methods to start, stop or restart the HTTP server.

  • constructor (httpOpts :object) :void

    Create the RWSHttpServer class instance.

    
    const { RWHttpServer } = require('regoch-websocket');
    const httpOpts = {
      port: 3211,
      timeout: 0 // if 0 the socket connection will never timeout
    };
    const rwHttpServer = new RWHttpServer(httpOpts);
    const httpServer = rwHttpServer.start(); // nodeJS HTTP server instance
    setTimeout(() => {
      rwHttpServer.restart();
      // rwHttpServer.stop();
    }, 3400);
          

  • start () :httpServer

    Start the HTTP server. Creates and return NodeJS HTTP Server instance.

  • async stop () :Promise<void>

    Stop the HTTP server.

  • async restart () :Promise<void>

    Restart the HTTP server.

Regoch Router

The "regoch-router" can be applied to separate code nicely and to build powerful, real-time API. It supports URL parameters and URL query string. The requirement is that server sent valid "jsonRWS" message with the cmd: 'route'.
Example: 000dev.js