To summarise the series, we wrote
- a WebSocket server on an existing express app
- setup individual and broadcast messages
- react hook to connect to server from a component
- setup authentication using a temporary token followed by jwt
What's next?
Our happy path is done, we can now focus on improving our code to better handling exceptions and make it more flexible to handle future use-cases.
Stalled connections
Setup a ping/pong system to occasionally check if a client is alive. Initiate a ping every x interval, if the client doesn't respond with a pong within the next run then terminate the connection. The same code can also be used to terminate clients who have connected but not sent a jwt payload.
// initiate a ping with client
// stalled or unauthenticated clients are terminated
function setupPing(clients) {
const interval = setInterval(() => {
for (let client of clients.values()) {
// terminate stalled clients
if (client.isAlive === false
|| client.is_authenticated === false) {
client.terminate();
}
// initiate ping
client.isAlive = false;
client.ping(() => {});
}
}, 5000);
return interval;
}
// similar to broadcast setup, with wss
setupPing(wss.clients);
// inside the "connection" code
ctx.on("pong", () => {
ctx.isAlive = true;
});
Routes
We can setup different WebSocket.Server instances for different routes and execute it based on the path that's passed in the initial connection. This is useful if you have websocket requirements which are very different from one another and you want to keep the logic clean and independent of each.
Before starting with this, you should move all the authentication code in to a utility file and move the websocket server code out of the initial upgrade logic to make code modular and reusable. This would become more clear when you see the source code.
// define all socket types and handling functions here
function setupSocketHandlers() {
return {
// functions on left will create a Websocket server,
// attache events and handle connections
"/demo": demoWss(),
"/notification" : notificationWss(),
};
}
// modify setup code
function setupWebSocket(server) {
const wssHandler = setupSocketHandlers();
server.on("upgrade",
/* ... */
// ignore request if path is invalid
const { path, token } = getParams(request);
if (!(path in wssHandler)) {
throw `Unknow conneciton path ${path}`;
}
/* ... */
// after token verification
const wss = wssHandler[path];
wss.handleUpgrade(request, socket, head, function done(ws) {
wss.emit("connection", ws, request);
});
)
Message Format
It's good to define a message format for the communication between client and server. Having a format ensures a more consistent behaviour and it's easier to stick to one format and handle it across your code.
// message format
{
type: "event",
data: { ...}
}
{
type: "notification",
data: {...}
}
Closing notes
After authentication make sure to turn off the on.message handler which does the authentication, otherwise it would always run when you receive messages. Registering a new message event will not overwrite an existing one. In my case, I kept the authentication function in the client itself and turned it off once jwt was verified.
// turn off jwt verfication message event
ctx.off("message", ctx.authenticate);
Logging is another aspect, I have not found anything like morgan for websockets. For now, I have most of the code in try/catch blocks which log messages to console.
Here are the links to server, client and the react hook:
I am yet to use websockets for a real workflow, I will keep this post updated with my findings once I do.
That's all folks.
Top comments (1)
Really thanks for this post series about websocket man. It saved lots of time for me.