Skip to content
 
 

Repository files navigation

encharm/cWS

Fast WebSocket implementation for Node.js and almost a drop-in replacement for the ws module

Important Notes

  • Consider using latest version of uWebSockets if you do not need compatibility with ws or a multi-process architecture.

  • This repository is a fork of ClusterWS/cWS

  • Prior, this repository was a fork of uWebSockets v0.14 therefore has two licence MIT and ZLIB

Supported Node Versions (SSL)

This table is true if you run ssl directly with cws (Node.js). In case if you use proxy for example nginx, cws can be run on bigger coverage.

cWS Version Node 26 Node 24 Node 22 Node 20
4.10.0 X X X X
4.9.0 X X X X
4.8.4 X X X X
4.8.3 X X X X
4.8.2 X X X X
4.8.1 X X X X
4.8.0 X X X X
4.7.0 X X X X
4.6.0 X X X X
4.5.3 X X X

Documentation

Useful links

Table of Contents

Installation

npm i @encharm/cws

Websocket Client

Typings: dist/client.d.ts

Import cws WebSocket:

const { WebSocket } = require('@encharm/cws');

Connect to WebSocket server:

const socket = new WebSocket(/* ws server endpoint **/);

Event on open is triggered when server accepts connection and this connection is ready to perform other actions:

socket.on('open', () => { });
// or like browser interface
socket.onopen = () => { };

Event on error is triggered if there is an error with the connection:

socket.on('error', (err) => { });
// or like browser interface
socket.onerror = (err) => { };

Event on close is triggered when connection has been closed:

// code: number and reason: string are optional
socket.on('close', (code, reason) => { });
// or like browser interface
socket.onclose = (code, reason) => { };

Event on message is triggered when client receives message(s):

// receives string or binary

socket.on('message', (message) => { });
// or like browser interface
socket.onmessage = (message) => { };

Event ping is triggered when other side sends ping to check if connection still active:

socket.on('ping', () => { });

Event pong is triggered after ping has been sent and it received back conformation:

socket.on('pong', () => { })

To send message use send function:

// send accepts string or binary
// will automatically identify type
socket.send(msg);

// will overwrite/specify message type to string (does not transform data to specified type)
socket.send(msg, { binary: false });

// will overwrite/specify message type to binary (does not transform data to specified type)
socket.send(msg, { binary: true });

// will call callback after message sent or errored
socket.send(msg, null, (err) => { });

To close connection you can use close or terminate methods:

// clean close code and reason are optional
socket.close(code, reason);
// destroy socket 
socket.terminate()

Use ping function to manually send ping:

socket.ping();
// now just wait for `pong` event

To get current socket ready state can use readyState getter:

socket.readyState; // -> OPEN (1) or CLOSED (3)

// check if socket open can be done by
if(socket.readyState === socket.OPEN) {}

// check if socket closed can be done by
if(socket.readyState === socket.CLOSED) {}

To find out number of buffered (and not yet sent) bytes:

socket.bufferedAmount;
// useful for implementing back-pressure

To get addresses use _socket getter:

socket._socket;

// Returns some thing like (all fields could be undefined):
// {
//  remotePort,
//  remoteAddress,
//  remoteFamily
// }

For more information check typings (*.d.ts) files in dist folder

Websocket Server

Typings: dist/server.d.ts

Import cws WebSocket:

const { WebSocket } = require('@encharm/cws');

Create WebSocket server:

const wsServer = new WebSocket.Server({ 
  /**
   * port?: number (creates server and listens on provided port)
   * host?: string (provide host if necessary with port)
   * path?: string (url at which accept ws connections)
   * server?: server (provide already existing server)
   * noDelay?: boolean (set socket no delay)
   * noServer?: boolean (use this when upgrade done outside of cws)
   * maxPayload?: number 
   * perMessageDeflate?: boolean | { serverNoContextTakeover?: boolean; windowBits?: number; memLevel?: number; threshold?: number }; level?: number }
   *   serverNoContextTakeover: false enables context takeover (the last 32 KB sent on a connection are referenced by later messages, ~20-40% fewer bytes on RPC-style streams). At level 1 (the fast built-in encoder) it costs 64 KB per connection; level 2 and above use zlib-ng (~256 KB per connection, better ratio, ~3x the CPU). Takeover connections compress fan-out messages once per subscriber instead of once per prepared message.
   *   true = shared compressor, no context takeover. serverNoContextTakeover: false = per-socket
   *   sliding window (streaming compression; far better on small messages). windowBits 9..15 and
   *   memLevel 1..9 pick its memory tier: 15/8 = ~256 KB per socket (default), 12/5 = ~32 KB, 10/3 = ~8 KB.
   *   threshold = minimum message size in bytes to compress (default 0); send(..., { compress }) overrides.
   *   Independent messages (the shared mode) are compressed by the built-in microdeflate encoder, ~1.7x faster
   *   than zlib-ng level 1 at the same ratio; CWS_MICRO_DEFLATE=0 falls back to zlib-ng.
   *   level 1..9 (default 1): deflate level of the per-socket compressor. The prebuilt bindings use zlib-ng
   *   (see `zlibBackend` export): level 1 is microdeflate (fast, fixed trees; with takeover a 64 KB per-connection window), level 2 and above zlib-ng, whose level 1 is its very fast "quick" strategy (~3-4x less CPU than zlib
   *   level 1 for ~8% worse ratio), level 2 matches zlib level 1's ratio.
   * verifyClient?: (info: ConnectionInfo, next: VerifyClientNext) => void (use to allow or decline connections)
   **/ 
 }, () => {
  // callback called when server is ready
  // is not called when `noServer: true` or `server` is provided
  // from outside
});

Event on connection is triggered when new client is connected to the server:

// `ws` is websocket client all available options 
// can be found in `Websocket Client` section above
// `req` is http upgrade request
wsServer.on('connection', (ws, req) => {})

Event on error is triggered when server has some issues and noServer is false:

// on error event will NOT include httpServer errors IF
// server was passed under server parameter { server: httpServer },
// you can register on 'error' listener directly on passed server
wsServer.on('error', (err) => { })

Event on close is triggered after you call wsServer.close() function, if cb is provided both cb and on close listener will be triggered:

wsServer.on('close', () => { })

To get all connected clients use clients getter:

wsServer.clients;

// loop thought all clients:
wsServer.clients.forEach((ws) => { });

// get number of clients:
wsServer.clients.length;

To send message to all connected clients use broadcast method:

// broadcast string
wsServer.broadcast(message);

// broadcast binary
wsServer.broadcast(message, { binary: true });

cWS supports auto ping with startAutoPing function:

wsServer.startAutoPing(interval, appLevel);

// send ping to each client every 10s and destroy client if no pong received 
wsServer.startAutoPing(10000);

// pass true as second parameter to run app level ping
// this is mainly used for browser to track pings at client level
// as they do not expose on ping listener 
// look for browser client side implementation at the bottom
// `Handle App Level Ping In Browser (example)`
wsServer.startAutoPing(10000, true);

To stop server use close function:

wsServer.close(() => {
  // triggered after server has been stopped
})

handleUpgrade is function which is commonly used together with noServer (same as ws module)

const wss = new WebSocket.Server({ noServer: true });
const server = http.createServer();

wss.on('connection', (ws, req) => { })

server.on('upgrade', (request, socket, head) => {
  wss.handleUpgrade(request, socket, head, (ws) => {
      wss.emit('connection', ws, request);
  });
});

For more information check typings (*.d.ts) files in dist folder

Write corking (performance)

All send() calls made to a socket during one event-loop iteration are framed immediately but written with a single gathered write (writev) when the iteration ends, instead of one syscall per send(). This is transparent to callers and preserves ordering; send() followed by close()/terminate() in the same tick still delivers the message. While writes are corked they are counted in bufferedAmount until the flush.

Set CWS_CORK=0 in the environment to disable it and write every message immediately (previous behaviour). Corking applies to plain TCP sockets only; TLS sockets always write immediately.

Send worker thread (performance)

The gathered write itself (the kernel's TCP work, which dominates a busy socket server's CPU) runs on a dedicated worker thread, not on the JavaScript thread. The end-of-tick flush hands each socket's frames to the worker through a lock-free queue; the worker performs the send and reports back, and completions, callbacks and bufferedAmount are handled on the main thread as before. Per-socket ordering is preserved, send() followed by close()/terminate() in the same tick still delivers, and a socket that closes while its send is in flight keeps its fd open until the send has finished. Measured on a busy 96-thread EPYC: 30-65% less main-thread CPU per message and 1.7-2.2x fan-out throughput, for 10-25% more total CPU on the worker.

With permessage-deflate enabled, compression of outgoing messages also runs on the worker: send() queues the raw payload and the worker deflates and frames it before writing, so a compressed send() costs the JavaScript thread the same as an uncompressed one (measured: 6 µs → 0.7 µs per 2 KB message).

Set CWS_SEND_THREAD=0 to disable it (sends and compression then happen on the main thread at the end of the tick). The sendThread export reports 'active' or the reason it is not. TLS sockets always send on the main thread.

Prepared messages (fan-out)

When one payload goes to many sockets with only a small per-socket prefix in front of it (an RPC header, a subscription id), prepare it once:

const { PreparedMessage } = require('@encharm/cws');

const prepared = new PreparedMessage(payloadBytes);          // copied into native memory once
for (const ws of subscribers) {
  ws.send(prepared, { prefix: headerFor(ws) });               // one frame: header + payload
}

send accepts a PreparedMessage wherever it accepts a buffer, plus a prefix option (a per-socket header, string or bytes) spliced in front of the payload inside the same frame. binary defaults to true for prepared messages; compress defaults to the server's threshold applied to the total length. The payload is never copied or compressed per socket: without deflate it is a second gather buffer in the end-of-tick write; with the shared compressor it is deflated once, on the first compressed send, and the prefix is spliced in front as a DEFLATE stored block. Sockets negotiated with context takeover (serverNoContextTakeover: false) and client sockets fall back to the regular path. Handles are released when garbage collected; sends in flight keep their own reference.

Compression stats

server.stats returns cumulative send counters since the module loaded:

const { messages, rawBytes, wireBytes, compressedMessages } = server.stats;
// rawBytes / wireBytes is the effective compression ratio; rawBytes - wireBytes the bytes saved.

rawBytes is what was handed to send, wireBytes what went out (framed, compressed where it applied). Whether a connection uses context takeover is visible in its handshake: the Sec-WebSocket-Extensions response omits server_no_context_takeover for takeover and includes it for shared mode.

Secure WebSocket

You can use wss:// with cws by providing https server to cws and setting secureProtocol on https options:

const { readFileSync } = require('fs');
const { createServer }  = require('https');
const { WebSocket, secureProtocol  } = require('@encharm/cws');

const options = {
  key: readFileSync(/** path to key */),
  cert: readFileSync(/** path to certificate */),
  secureProtocol
  // ...other Node HTTPS options
};

const server = createServer(options);
const wsServer = new WebSocket.Server({ server });
// your secure ws is ready (do your usual things)

server.listen(port, () => {
  console.log('Server is running');
})

For more detail example check examples folder

Handle App Level Ping In Browser (example)

Handling custom App level ping, pong from the client side which does not have onping and onpong listeners available such as browsers.

Note if all your clients have onping and onpong listeners do not send appLevelPing ping from the server. If you enable appLevelPing you will need to implement similar handler for every client library which connects to the server.

const PING = 57;
const PONG = new Uint8Array(['A'.charCodeAt()]);

socket.binaryType = 'arraybuffer';

socket.onmessage = function (message) {
    // note actually sent message in
    // browser default WebSocket is under `message.data`

    // check if message is not string 
    if (typeof message.data !== 'string') {
        // transform it to Uint8Array
        let buffer = new Uint8Array(message.data);

        // Check if it is actually ping from the server
        if (buffer.length === 1 && buffer[0] === PING) {
            // this is definitely `ping` event you can call custom on ping handler 
            // also must send back immediately pong to the server 
            // otherwise server will disconnect this client
            return socket.send(PONG);
        }
    }

    // process with your logic
}

About

Fast WebSocket implementation for Node.JS (drop-in replacement for ws)

Topics

Resources

Stars

3 stars

Watchers

1 watching

Forks

Used by

Contributors

Languages