WS + API: A NodeJS Server
Designing a Node Server with Web Sockets and RESTful API
- Part 1: One Server To Rule Th…Most of Them
- Part 2: WS + API: Getting Started
- Part 3: WS + API: Enter the Index.ts
- Part 4: Interlude: Protocols
- Part 5: WS + API: Web Socket Handler
- Part 6: WS + API: Getting the Message
- Part 7: Interlude: Redis & Redis-OM
- Part 8: WS + API: Actions Speak Louder Than Code
- Part 9: From Node to Deno
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.