Build a websocket server using express & ws package

Build a websocket server using express & ws package

This is yet another kind of NodeJs websocket tutorial that i was hoping to talk about my experience in building a websocket server and implementing a pub/sub pattern to send and receive events the same way socket.io is providing but we here we're using the simple to use and blazing fast ws package.

Why not socket.io?

Well first it has all you might need, and i mean all like all of it, which is kinda a lot since you end up using a lot of stuff you don't actually need, while on the other hand ws provides you with the base functionality and the ability to customize it as per needs

Besides i remember times when socket.io repository was abandoned, and they had a lot of issues, and they it was kinda deprecated in favor of engine.io and made a great role also in choosing ws over it.

HTTP server setup

Now let's setup our express HTTP server a typical implementation with nothing fancy

const express = require('express')
const app = express()
const PORT = 3000 || process.env.PORT

app.get('/', (_, res) => {
  res.send('Welcome to main page')
})

app.listen(PORT, () => {
  console.log(`Server is working on http://localhost:${PORT}`)
})

Nothing special here, we defined our index endpoint which is going to return Welcome to main page if we went to http://localhost:3000/

So to setup our websocket server we need to import ws package and then tweak this a little bit

const express = require('express')
const WsServer = require('ws')
const { createServer } = require('http')

const app = express()
const server = createServer(app)

function initWs() {
  const options = {
    noServer: true
  }

  return new WsServer.Server(options)
}

function initHttpServer(port) {
  app.set('view engine', 'ejs')

  app.get('/', (_, res) => {
    res.render('index')
  })

  server.listen(port, () => {
    console.log(`Server is working on http://localhost:${port}`)
  })

  return app
}

function initWebSocketServer(port = 3000) {
  initHttpServer(port)
  const wss = initWs()

  server.on('upgrade', async(req, socket, head) => {
    try {
      wss.handleUpgrade(req, socket, head, (ws) => {
        // Do something before firing the connected event

        wss.emit('connection', ws, req)
      })
    } catch(err) {
      // Socket uprade failed
      // Close socket and clean
      console.log('Socket upgrade failed', err)
      socket.destroy()
    }
  })

  return wss
}

const wss = initWebSocketServer()

wss.on('connection', (ws) => {
  ws.on('message', (data) => {
    ws.send(data)
  })
})

Now we splitted the code into 3 functions, 1 to handle preparing our websocket headless server by just passing noServer: true to ws server constructor, beside a function to setup our http server and notice that we added new route

  app.get('/', (_, res) => {
    res.render('index')
  })

This to handle our client that is going to be connected to our server and thats where our client code will live on index view, where it exists under views directory here

websocket-server/
├─ views/
│  ├─ index.ejs
├─ server.js

Client side setup

Now on the client side the code is pretty simple

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>WS Test</title>
  </head>
  <body class="container">
    <script>
      let keepAliveId = null
      function connect({ url, keepAlive, keepAlivePeriod }) {
        const ws = new WebSocket(url)

        if (keepAlive) {
          clearInterval(keepAliveId)
          keepAliveId = setInterval(() => {
            ws.send('ping')
          }, keepAlivePeriod || 5000)
        }

        ws.onopen = () => {
          console.log('Websocket is open')
        }

        ws.onclose = () => {
          console.log('Websocket closed')
        }

        ws.onerror = (err) => {
          console.log('Websocket error', err)
        }

        ws.onmessage = (message) => {
          console.log('Websocket data', message.data)
        }
      }

      connect({
        url: 'ws://localhost:3000',
        keepAlive: true,
        keepAlivePeriod: 5000
      })
    </script>
  </body>
</html>

We have our connect function here that expects the following options { url, keepAlive, keepAlivePeriod }

keepAlive property is to keep the websocket connection alive by sending message after a period of milliseconds which what in the keepAlivePeriod for

The way we're keeping the websocket alive is in this snippet

        if (keepAlive) {
          clearInterval(keepAliveId)
          keepAliveId = setInterval(() => {
            ws.send('ping')
          }, keepAlivePeriod || 5000)
        }

So every period of time we're sending this ping message to server and thus server is going to echo back what we're sending and this is how simply the socket connection is alive

That’s basically how we created a simple echo server using just ws package with even the ability to keep the socket connection alive. Later on I will explain how to trigger events from client side and even subscribe to these events so whenever server is emitting an event it will be received by the client