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.
If everything goes well, we should see the connected message in our connection activity 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.
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)
})