Websockets: Blazing Fast Data Exchange

WebSockets are a protocol for establishing long-lasting connections between several nodes. Once the connection is established via a handshake, all subsequent messages are sent immediately. Not needing to wait for request-response pairs, as in the HTML protocol, greatly increases transmission speed. The connection is full-duplex, meaning data can be received and send at the same time, in both directions. In summary, these capabilities allow real-time data exchange between several nodes. WebSockets are the foundation for video streaming, audio streaming and chat applications.

While working on a new application, I discovered WebSockets as a protocol and facilitator for instantaneous, constant data exchange. I also discovered a flexible, event-driven programming style that enables parts of a web-application to re-render itself whenever new data is received. This makes it great for highly interactive applications as well.

In this article, you will get a general overview about WebSockets and see how an example plain JavaScript application with client and server is setup using the socket.io framework.

This article originally appeared at my blog.

How WebSockets Work

> curl -vv -X GET /socket.io/?EIO=3&transport=websocket&sid=SZYqXN8Nbv5nypCiAAAIAccept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Sec-WebSocket-Version: 13
Origin: http://127.0.0.1:2406
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: dXjMvP0KSh3Ts3ZgWh6UpA==
Connection: keep-alive, Upgrade
Upgrade: websocket

The server then returns a connection upgrade response.

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: wogWuZGEra8NGMeREAPru5yDTDA=
Sec-WebSocket-Extensions: permessage-deflate

And then, the WebSocket connection between the client and server is created.

WebSocket messages are simple data: strings, structured, data, or binary. You can see the exchanged messages with a suitable browser, for example with the Firefox Developer Tools.

NodeJS Example

General Setup

websocket
├── client
│ ├── index.js
│ ├── node_modules
│ ├── package-lock.json
│ └── package.json
└── server
├── index.js
├── node_modules
├── package-lock.json
└── package.json

Implementing the Server

In the file index.js, we start an express server instance.

const express = require('express')app = express()app.get('/', (req, res) => {
res.send('WebSocket Test')
})
const backendServer = app.listen(3000, () => {
console.log(`BOOTING BACKEND on port 3000`)
})
const websocket = require('socket.io')

Now we add socket.io to our server. In the above snipped, we created the backendServer object, an instance of HttpServer. Socket.io needs this object to bind its functions and add an endpoint to which clients can connect. We pass this object to the Socket.io constructor, together with an optional config object. Out of the box, socket.io does a great job autoconfiguring itself. If you need to customize the connection details, take a look at the official documentation.

const websocket = require('socket.io')const config = {
serveClient: true,
pingInterval: 10000,
pingTimeout: 5000,
cookie: true
}
const io = websocket(backendServer, config)

Now, the server is ready, but does not provide any functionality yet. Let’s see how to setup the client.

Implementing the Client

First, we create an Express server instance and add socket.io. Additionally, we also deliver static HTML from the html directory.

const express = require('express')
const path = require('path')
const websocket = require('socket.io')
const app = express()app.use('/', express.static(path.join(__dirname, 'html')))app.get('/health', (req, res) => {
res.send('ok')
})
frontendServer = app.listen(8080, () => {
console.log(`BOOTING FRONTEND on port 8080`)
})
io = websocket(frontendServer)

Second, we add the socket.io JavaScript client to the HTML page that the express servers delivers.

<head>
...
<script src="/socket.io/socket.io.js"></script>
</head>

And finally, we establish the connection to the backend server by adding — for simplicity — an inline script declaration to the index.html file.

<head>
...
<script>
const socket = io('ws://localhost:3000')
</script>
</head>

Now, client and server are connected.

Exchanging Messages between Client and Server

Lifecyle events concern the lifecyle of the connection. The first event connect establishes the connection. If for any reasons the connection is disrupted by a networking issue, then a connectError is created, followed by reconnects event to re-establish the connections. Finally, clients can explicitly disconnect. See also the full lifecycle diagram.

To let the server log a message upon being connected, you add the following code to the file server/index.js.

io.on('connection', socket => {
console.log(`+ client ${socket.id} has connected`)
})

Custom events are designed by the application. An event needs a name, and optionally a payload that is transmitted. This event-name is used in two places: One node emits an event, and other nodes listen to this event.

Lets implement the periodic sending of the current server time to the client.

In server/index.js, set a 5 seconds interval to send the time.

io.on('connection', (socket) => {
# ...
setInterval( () => {
socket.emit('api:server-time', new Date().toTimeString());
}, 5000)
});

And in the file client/html/index.html, add an event listener. Upon receiving the event, the defined callback function will be executed. In this example, the function will manipulate the DOM to show the server time, and it will also log the received server time to the console-

<script>
const socket = io('ws://localhost:3000');
socket.on('api:server-time', function (timeString) {
console.log("Update from Server", timeString);
el = document.getElementById('server-time')
el.innerHTML = timeString;
});
</script>

Exchange Server Time: Complete Source Code

Server

const express = require('express')app = express()app.get('/', (req, res) => {
res.send('WebSocket Test')
})
const backendServer = app.listen(3000, () => {
console.log(`BOOTING BACKEND on port 3000`)
})
const websocket = require('socket.io')const config = {
serveClient: true,
pingInterval: 10000,
pingTimeout: 5000,
cookie: true
}
const io = websocket(backendServer, config)io.on('connection', socket => {
console.log(`+ client ${socket.id} has connected`)
setInterval(() => {
socket.emit('api:server-time', new Date().toTimeString())
}, 5000)
})

Client

const express = require('express')
const websocket = require('socket.io')
const app = express()app.use('/', express.static(path.join(__dirname, 'html')))app.get('/health', (req, res) => {
res.send('ok')
})
frontendServer = app.listen(8080, () => {
console.log(`BOOTING FRONTEND on port 8080`)
})
io = websocket(frontendServer)

client/html/index.html

<!doctype html>
<html>
<head>
<title>WebSocket Demo</title>
<meta charset="utf-8">
<link rel="stylesheet" href="css/default.css">
</head>
<script src="/socket.io/socket.io.js"></script>
<body>
<section>
<h1>Server Time</h2>
<p>The current server time is:</p>
<div id="server-time" />
</section>
<script>
const socket = io('wss://localhost:3000');
socket.on('api:server-time', function (timeString) {
console.log("Update from Server", timeString);
el = document.getElementById('server-time')
el.innerHTML = 'Server time: ' + timeString;
});
</script>
</body>
</html>

Conclusion

WebSocket’s are an interesting mechanism for a constant connection between server and client. This connection enables instantaneous, event driven data exchange for texts, structured data such as JSON, and even binary data. In JavaScript applications, combining CommonJS and Web APIs, especially the DOM API, you can design very interactive web pages. I was surprised how easy it is to have a basic single-page-applications in which different web page parts send and receive events to updates its DOM. I’m looking forward to use WebSockets more often in future applications.

IT Project Manager & Developer