Skip to content

Websockets: Blazing Fast Data Exchange

By Sebastian Günther

Posted in Tech, Websockets

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.

How WebSockets Work

WebSockets are based on the HTTP protocol. Either via port 80, or encrypted via port 443, client and server perform a handshake. This handshake has the form of an OPTIONS request from client to server. Here is an example of how the HTTP header look like.

> curl -vv -X GET /socket.io/?EIO=3&transport=websocket&sid=SZYqXN8Nbv5nypCiAAAI

Accept: */*
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

In this section I will show how to setup a client to server WebSocket connection using the [socket.io](https://socket.io/) library. The idea for this example is based on a great tutorial of Heroku: The server will send periodic time updates to the client, and the client renders the time information in a clock widget.

General Setup

First of all, create two folders client and server, and put an index.js file on each. Then, initialize an npm package in each project with npm init. Answer the raised questions as you like. Finally, use npm i socket.io express in each project. After these steps, your directory layout should look like this:

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

Implementing the Server

The server has two responsibilities: To accept incoming client connections, and to send time-information to all registered clients. The server itself will be written with Express.

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

The client follows similar steps as before.

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

Messages between client and server are based on events. There are two sources for events: lifecycle and custom events.

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

Here is the complete source code for this example.

Server

server/index.js

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

client/index.js

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

This article showed how to implement an example WebSocket application in which the server sends the current time to its connected clients. Setup and configuration of a WebSocket connection becomes very easy with using the socket.io library.

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.