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