WebSocket demo server in Node.js

In this article we will look into how to build a demo Node.js server that serves example stocks data over WebSocket. If you are new to WebSockets, you can read more in the WebSocket Protocol specification.

To follow along, make sure you have Node.js and npm installed. Our preferred method is to use nvm to manage multiple versions and switch between them. You can read more about nvm from there official page.

You can find the code for this article in Stocks-WebSocket-Demo-Server repository on GitHub.

# Setup

Choose a location on your system to store the project folder and go to that location in the command line. Create a new folder for your project and create a package.json file. Answer questions in the console to configure your package.json file or skip them to keep the defaults.

mkdir StocksWebsocketDemoServer && cd StocksWebsocketDemoServer
npm init

Open the newly created project folder in the code editor of your choice. Our package.json file looks like so at the start.

{
  "name": "stockswebsocketdemoserver",
  "version": "1.0.0",
  "description": "This is a demo server that serves example stocks data over WebSocket.",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "NATALIA PANFEROVA",
  "license": "MIT"
}

# Create a WebSocket server

For our server we will use a WebSocket library called ws. Install it with npm and save as a dependency.

npm install ws --save

Then add a server.js file and create a WebSocket server listening on port 8080.

let WebSocketServer = require('ws').Server

let wss = new WebSocketServer({port: 8080})
console.log("WebSocket server is listening on port 8080.")

# Example stocks data

For demo purposes, we will create some fake stocks data with the help of fake-stock-market-generator library.

First we need to install the dependency.

npm install fake-stock-market-generator --save

Then we will use it to write a script in a separate file called generateStocks.js that will generate an array of stock objects and save them to a file.

let fakestockmarketgenerator = require('fake-stock-market-generator')
let fs = require("fs")

let stocks = [...Array(20).keys()].map(i => {
    return fakestockmarketgenerator.generateStockData(10)
})
fs.writeFileSync('stocks.json', JSON.stringify(stocks, null, 4));

We will add this script to our package.json file in the scripts property.

"scripts": {
    "generate-stocks-data": "node generateStocks.js"
}

Now we can run it from the command line.

npm run generate-stocks-data

This will create a file called stocks.json that contains an array of stock objets.

In our server.js file we will now read the stocks data and save it in a variable, so that our server can later serve that data to clients.

let WebSocketServer = require('ws').Server
let fs = require("fs")

let stocks
try {
    stocks = JSON.parse(fs.readFileSync("stocks.json"))
    console.log("Successfully loaded stocks data.")

} catch {
    throw Error("Could not load stocks data.")
}
let stockSymbols = stocks.map(stock => stock.symbol)
console.log(`Supported stock symbols: ${stockSymbols}`)

let wss = new WebSocketServer({port: 8080})
console.log("WebSocket server is listening on port 8080.")

# WebSocket demo API

Clients will be able to interact with our server with JSON text messages. They will connect to the server and subscribe to particular stock updates. To test our server as we write it, we will use Cleora - HTTP & WebSocket Client.

The full code for the server.js file described here can be found on GitHub. We will go through the main parts.

# Establish a connection

When a new connection to the server is initiated, the server will send a message listing available stock symbols.

let getConnectedEvent = () => {
    let event = {
        event: "connected",
        supportedSymbols: stockSymbols,
        message: "All stocks data is not real and is generated solely for demo purposes."
    }
    return JSON.stringify(event)
}

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

To try it out, start the server in the command line.

node server.js

Then in Cleora app create a new WebSocket request. Enter your IP address and port for your local server in the URL field and open a new connection.

Cleora app screenshot with server url being entered into the url text field

If everything goes well, we should see the connected message in our connection activity view.

Cleora app screenshot showing the active connection view

# Receive client messages

We will expect two kind of messages from the client: to subscrive to specific stock updates and to unsubscribe from specific stock updates. We will first check that the message is not too long, so that we don't waste server resources parsing it, then parse the message and handle it according to the message event type. If we get a message that we don't expect, we will send an error message back to the client.

wss.on('connection', (ws) => {
    ws.send(getConnectedEvent())

    let connectionInfo = {
        stocksToWatch: []
    }
    

    ws.on('message', (message) => {
        connectionInfo.isActive = true

        if (message.length > 300) {
            ws.send(getErrorEvent("message too long"))
            return
        }

        let parsedMessage
        try {
            parsedMessage = JSON.parse(message)
        } catch {
            ws.send(getErrorEvent("invalid message"))
            return
        }

        if (parsedMessage.event === "subscribe") {
            handleSubscribe(ws, parsedMessage, connectionInfo)
        } else if (parsedMessage.event === "unsubscribe") {
            handleUnsubscribe(ws, parsedMessage, connectionInfo)
        }
    })
})

# Send updates to the client

The server will send updates every 10 seconds if the array of stocksToWatch is not empty. It will set an interval when a client connects and clear the interval when the connection is closed.

let connectionInfo = {
    stocksToWatch: [],
    stocksUpdateCount: 0
}

ws.stocksInterval = setInterval(() => {
    if (connectionInfo.stocksToWatch.length > 0) {
        ws.send(getStocksUpdateEvent(connectionInfo))
        connectionInfo.stocksUpdateCount += 1
    }
}, 10000)

ws.on('close', () => {
    clearInterval(ws.stocksInterval)
})

To test the updates we will subscribe to IET and ZHT stocks by sending a message that looks like this.

{
  "event": "subscribe",
  "stocks": ["IET", "ZHT"]
}

We can see that the updates for those stocks are now coming from the server every 10 seconds.

Cleora app screenshot showing the active connection view with WebSocket messages

To unsubscribe from the updates we can send an unsubscribe message.

{
  "event": "unsubscribe",
  "stocks": ["IET", "ZHT"]
}

After that we should stop receiving the updates.

# Monitor the connection status

To make sure that we don't keep an inactive connection open for too long our server will send ping frames every 15 seconds. If the client doesn't respond with a pong frame, the connection will be closed on the next iteration of the interval.

ws.pingInterval = setInterval(() => {
    if (!connectionInfo.isActive) {
        disconnect(ws, "connection inactive")
    } else {
        connectionInfo.isActive = false
        ws.ping()
    }
}, 15000)

ws.on('pong', () => {
    connectionInfo.isActive = true
});

ws.on('close', () => {
    clearInterval(ws.pingInterval)
    clearInterval(ws.stocksInterval)
})

Because this is just a demo server that anyone can connect to, we don't want to keep any one connection open for more than 5 minutes to avoid a big load on the server. To achieve this we will set a connection timeout.

ws.connectionTimeout = setTimeout(() => {
    disconnect(ws, "connection time exceeds 5 minutes")
}, 300000)

ws.on('close', () => {
    clearInterval(ws.pingInterval)
    clearInterval(ws.stocksInterval)
    clearTimeout(ws.connectionTimeout)
})