WS + API: A NodeJS Server

Designing a Node Server with Web Sockets and RESTful API

Our main entry point sets up our combined HTTP(S) and WS server, but attempting to communicate with the server using either protocol will result in no response. In this post, we’re going to look at the middleware that allows the server to process web socket communications from the client to the server.

// Modules
const WSMessageHandlers = require('../Utils/WSMessageHandlers');

// Types
import { WebSocketServer, WebSocket } from "ws";

// Class definition
module.exports = class SocketServer {
    static _wss: WebSocketServer;
    static _wsc:typeof WSMessageHandlers;

    constructor(wss: WebSocketServer){
        if (!(SocketServer._wss instanceof WebSocketServer)) SocketServer._wss = wss;
        if (!(SocketServer._wsc instanceof WSMessageHandlers)) SocketServer._wsc = new WSMessageHandlers();
    }

    Report(){
        // Console info connection stats on demand. 
    }

    Init(){
        console.log(`Socket server waiting for connections...`);
        SocketServer._wss.on('connection', (ws: WebSocket)=>{
            console.log(`Connect`)
            
            //##################################################################
            ws.on('error',console.error);

            //##################################################################
            ws.on('close', (code:number, data:Buffer)=>{
                console.log(`Disconnected: Code ${code}  Reason: ${data.toString() || 'unknown'}`)
            })

            //##################################################################
            ws.on('message', (msg:Buffer)=>{
                SocketServer._wsc.HandleMessage(ws, msg.toString());
            })
            
        });
        
    }
}

Our Imports

We have one module import: WSMessageHandlers. We won’t be discussing this in this post, but know that this is actually the core response module for routing messages to handlers received through web sockets. We also have a few type imports for WebSocketServer and WebSocket which allow us to keep our arguments and variables honest.

SocketServer Class

The SocketServer class provides us with a series of methods that use the wsServer we created in the entry point of the server. As of right now, we only have two populated methods: a constructor and an Init.

Static Variables

At the top of the class we have two static variables: _wss and _wsc. These will hold the socket server created in the entry point and and instance of the class that holds our message handlers.

I made these “static” which should result in only one instance of each. The constructor method sets up the actual data for these variables. _wss is assigned the instance of the wsServer that we created and passed to the class in the entry point file, and _wsc is an instance of the WSMessageHandlers class we loaded at the top of this file.

Init()

When working with the ws library, any communication from the client is handled as an event. Before we can handle most of these events, we need to ensure we have a connection to a client. This is why we start out with one event handler for connection.

In the connection event handler we receive a WebSocket argument. This holds information about the currently connecting client. We must keep track of this object throughout the lifecycle of the connection because we’ll use it to identify which client is sending messages to the server, and for the server to send messages back to the client. The code isn’t present right now, but we will be storing this value in an in-memory instanced collection.

Once inside the connection handler, we can define other event handlers for events raised through this specific WebSocket instance. This is important: the connection event is handled by the WebSocketServer, but other events are handled through individual instances of WebSocket.

We have a basic error event handler, which currently displays the error to the console. Eventually, we will want to log this and, where possible, alert the client. This is not just for handling connection errors, but any kind of web socket error.

Next we have the close event. This is an important event for the server, but not so much the client as by the time the server receives the event and is ready to respond, the client has already been disconnected and will not receive any messages we send to them. When a client disconnects (by their action, as we can institute a re-connection protocol if we desire), the server will probably want to clean up information about that client that we’ve been keeping; this is where we’d want to do that.

Finally, the bulk of our processing will happen in relation to a generic message event. When the server receives a message that isn’t an intrinsic connection, error, or close event (there are a few other intrinsic events we aren’t covering here), the payload received as msg should be shaped in such a way that the server can parse it and determine what to do with it. If all clients simply posted a message with a value of “hello world” and expected a wealth of different responses, they’d be bummin’ because the server could only take a single action based on “hello world”. Because of this, I’ve started shaping an expected message form that clients will send to the server as a message event:

interface IMessage {
    Name: string;
    Description?: string;
    Data?: any;
}

Yeah, this is not its final form. The message needs a Name which will be something specific that the server recognizes, like “CHAT” or “UPDATE”. Description is probably unnecessary, and I can’t think of any reason it should be there. Data, however, is important. In order for a message to impart information, it has to be carried via some property of this interface instance. Because a message can be dynamic, it serves as a general purpose vehicle for different types of messages. A CHAT message may have a “from”, “to”, and “content” properties, while an UPDATE message may have “action” describing what got updated. Casting Data as any allows me to pass different forms of data using the same standard wrapper.

The inbound msg object is cast as a string (because the argument is of type Buffer which is naturally non-human readable) and is passed to our WSMessageHandlers.HandleMessage method which will deal with the specific message as specified by its Name property.

As always, this is a work in progress.

Conclusion

As I wanted the entry point to be as thin as possible, the task of handling web socket inbound communication has been pushed into its own middleware file. It’s main purpose is to receive and respond to one of the four main events: connection for new clients, error to handle any web socket related errors, close to handle clean up when a client disconnects, and message which will be responsible for receiving and parsing messages from the client.

Series Navigation<< Interlude: ProtocolsWS + API: Getting the Message >>

Scopique

Husband, father, gamer, developer, and curator of 10,000 unfinished projects.