Websockets with NodeJS and µWebSockets.js — Part 1

3/7/2020

This is Part 1 of a small series of posts to get a websocket server powered by µWebSockets.js and a solid client connection using standard websockets.

This post covers getting the skeleton of the websocket server running. In Part 2 we'll build a small client library for easily establishing a persistent websocket connection with an infinite reconnection strategy. Finally, in Part 3 we'll flesh out the server with client management capabilities.

I encourage you to follow along in your own IDE. A GitHub project is also available with tags at each pinnacle commit.

Websocket Server

First we'll need to install µWebSockets.js.

npm install uNetworking/uWebSockets.js#v17.3.0

With that installed we're ready to get our server up and running.

Create index.js, require uWebSockets.js, and construct the websocket server.

// index.js

const uWS = require('uWebSockets.js')

const app = uWS.App()
.ws('/*', {
  // New connection established
  open: (socket, req) => {},
  // Connection closed
  close: (socket, req) => {},
  // Client request received
  message: (socket, req, isBinary) => {}
})

const PORT = 9001
app.listen(PORT, (listenSocket) => {
  if (listenSocket)
    console.log(`Listening to port ${PORT}`)
})

At this point you should be able to run your server using the command node index.js. Unfortunately it doesn't do much. Let's fix that by implementing a simple, classic ping -> pong between the client and the server.

When the server receives a ping message from the client, it'll echo back the payload in a pong response.

message: (socket, message) => {
  const { action, data } = JSON.parse(decoder.write(Buffer.from(message)))

    switch (action) {
      case 'ping':
        socket.send(JSON.stringify({ action: 'pong', data }))
        break
    }
}

WhooOOOa there. What's with the fancy parse, decoder, Buffer jarble there at the top?

Well, because our client request comes to us in the form of an ArrayBuffer (a.k.a. byte array), looking something like [123, 34, 97, 99, ...] and so on. We, on the other hand, are expecting to work with nice, pretty JSON. So how do we get there?

In order to read the contents of the buffer we need to create a TypedArray from the Buffer (Buffer.from(message)), decode that buffer into a JSON string (decoder.write(...)), and finally parse it as JSON (JSON.parse(...)).

Phew! Let's hope we don't run into too many more of those. 😅

So what's the rest of our message function doing?

We're expecting to receive a JSON object from the client with two properties: action & data. The action will indicate to the server which action it's calling along with the appropriate data payload for that action.

We only have one action defined - ping - which the server aptly responds to with a pong message of its own, echoing back the data payload we sent to it.

Now we need a client to connect to our server. You could spend the time spinning up a small http server...or you could open up the Dev Tools in your web browser. Let's do that 😉

First, start your websocket server. If it's already running, make sure you restarted it after adding the new message code.

With a browser open (a modern one of course), open up its Dev Tools and paste in the following:

// establish connection
const PORT = 9001
let ws = new WebSocket(`ws://localhost:${PORT}`)

ws.onopen = function onOpen() {
  console.log('ws connection established!')
}

ws.onclose = function onClose() {
  console.log('ws connection closed')
}

ws.onmessage = function onMessage(packet) {
  const { action, data } = JSON.parse(packet.data)
  switch (action) {
    case 'pong':
      console.log(data)
      break;
  }
}

NOTE: If you get an error complaining about violation of Content Security Policy, try a different site. Google.com works great. So does this site 😉

Immediately you should see "ws connection established!" Now, send a ping up to the server.

ws.send(JSON.stringify({ action: 'ping', data: 'pong' }))

Remember that our server is expecting an object in the shape of { action, data }. The message also needs to be a string, so we JSON.stringify() it.

After sending the message you should see "pong" in your console. Congratulations! You're playing a game with your server! 🏓

You can send up any valid JSON as data. Try an object, and see it echoed back to your console!

At some point you may notice when you go to send a message that it complains that the websocket connection has been lost:

WebSocket is already in CLOSING or CLOSED state.

What happened? How do we prevent that?

In Part 2 we'll build up a robust client library for establishing connections to our websocket server that will prevent idle timeouts and gracefully recover from disconnects.