A Socket.io tutorial that ISN'T a chat app (with React.js)

Recently, a friend asked for advice for displaying sensor data in real-time (or as near-real-time as possible) on to a dashboard. Specifically, he'd be reading OBD2 data from a drag racing car and wanted to display some data points to an external system. After considering a message bus like Kafka, we decided it was way over kill. I recommended using websockets, specifically, the Socket.io implementation of them. To be fair, I'd never used the Socket.io library, I'd only read about them and I had a surprisingly difficult time finding information on how to use them outside of a chat app or a multiplayer game. To me, translating the chat-room app tutorial found on Socket.io into a different use case was a bit of a process, so I decided to write up this little demo which may be useful to someone in thinking about sockets a different way.

The 3 layers

Here's the super high level design we came up with:



For our prototype, well be simulating the sensor/reader, setting up a backend server as a pass-through, and implementing a front end consumer.

The Backend

The two packages we'll need are:

const express = require ( "express" ); const http = require ( "http" ); const socketIo = require ( "socket.io" ); //Port from environment variable or default - 4001 const port = process . env . PORT || 4001 ; //Setting up express and adding socketIo middleware const app = express (); const server = http . createServer ( app ); const io = socketIo ( server ); //Setting up a socket with the namespace "connection" for new sockets io . on ( "connection" , socket => { console . log ( "New client connected" ); //Here we listen on a new namespace called "incoming data" socket . on ( "incoming data" , ( data ) => { //Here we broadcast it out to all other sockets EXCLUDING the socket which sent us the data socket . broadcast . emit ( "outgoing data" , { num : data }); }); //A special namespace "disconnect" for when a client disconnects socket . on ( "disconnect" , () => console . log ( "Client disconnected" )); }); server . listen ( port , () => console . log ( `Listening on port ${ port } ` ));

Lets break this down:

const express = require ( "express" ); const http = require ( "http" ); const socketIo = require ( "socket.io" ); //Port from environment variable or default - 4001 const port = process . env . PORT || 4001 ; //Setting up express and adding socketIo middleware const app = express (); const server = http . createServer ( app ); const io = socketIo ( server );

If you've used express before, most of this isn't anything new. The only socket.io related stuff we see here is const io = socketIo(server); which sets up a new server instance of socket.io.

//Setting up a socket with the namespace "connection" for new sockets io . on ( "connection" , socket => { console . log ( "New client connected" ); //Here we listen on a new namespace called "incoming data" socket . on ( "incoming data" , ( data ) => { //Here we broadcast it out to all other sockets EXCLUDING the socket which sent us the data socket . broadcast . emit ( "outgoing data" , { num : data }); }); //A special namespace "disconnect" for when a client disconnects socket . on ( "disconnect" , () => console . log ( "Client disconnected" )); }); server . listen ( port , () => console . log ( `Listening on port ${ port } ` ));

Here we're setting up a socket namespace called connection which is where clients will connect to. Once an initial connection is made, we listen on two new namespaces. incoming data and disconnect . The first one is where our "producer" or sensor/reader will be pushing data too.

In our callback we call socket.broadcast.emit("outgoing data", {num: data}); . The broadcast flag is special because it allows us to emit data to every client EXCEPT the one that sent us the data. There's no point in sending data back to the producer so we broadcast on yet another namespace, outgoing data .

You'll notice we serialize our incoming data before pushing to our outgoing data namespace. This will make it cleaner on our front end and will give you an idea about how we can send multiple data points in one emit.

The disconnect namespace is reserved for when a client loses connection. It is a good place to do any cleanup. For example, if your server is keeping track of clients that are connected, its a good spot to change the state of the client to disconnected.

The final line is setting up our express app to start listening.

The simulated sensor

Since this is a simulation, all we need to do is send some random data. For the prototype, this was done in pure node.js but there are many client libraries available for socket.io that would most certainly be better for running on an Arduino or other micro-controllers that would attach to a OBD2 sensor. Don't roast me too hard here, it's just a demo.

For this demo, I'll be demoing a "speed" value.

The only package we used here is socket.io-client.

let socket = require ( 'socket.io-client' )( 'http://127.0.0.1:4001' ); //starting speed at 0 let speed = 0 ; //Simulating reading data every 100 milliseconds setInterval ( function () { //some sudo-randomness to change the values but not to drastically let nextMin = ( speed - 2 ) > 0 ? speed - 2 : 2 ; let nextMax = speed + 5 < 140 ? speed + 5 : Math . random () * ( 130 - 5 + 1 ) + 5 ; speed = Math . floor ( Math . random () * ( nextMax - nextMin + 1 ) + nextMin ); //we emit the data. No need to JSON serialization! socket . emit ( 'incoming data' , speed ); }, 100 );

Most of this should be pretty self-explanatory, so this section will be short.

let socket = require('socket.io-client')('http://127.0.0.1:4001'); sets up the package to be used. We start out setting out speed variable to 0.

let socket = require('socket.io-client')('http://127.0.0.1:4001'); returns to us the socket connection to be used. We're telling it where its running and what port its running on.

I used setInterval here to simulate read requests between a micro-controller and a sensor every 100 milliseconds. The math to set the next speed is just a "hack-y" way to increase or decrease the speed slightly every time and not to allow the speed to be over 140 or below 0.

socket.emit('incoming data', speed); is where we emit the data through the socket. We emit the data on the incoming data namespace which we've set up on the backend in the previous section.

That's it! Cool huh?

The Dashboard

I built this in React and it was super easy and quick. I'm not going to get into the React details as its out of scope. I'm going to focus on how to consume the data from a socket. That being said, I used react-d3-speedometer to display a speedometer. I've gotta say I'm really impressed with the way it look! I'm also using the same socket.io-client package that we used on the producer.

Here's the React component:

import React , { Component } from "react" ; import socketIOClient from "socket.io-client" ; import ReactSpeedometer from "react-d3-speedometer" class App extends Component { constructor () { super (); this . state = { response : 0 , endpoint : "http://127.0.0.1:4001" }; } componentDidMount () { const { endpoint } = this . state ; //Very simply connect to the socket const socket = socketIOClient ( endpoint ); //Listen for data on the "outgoing data" namespace and supply a callback for what to do when we get one. In this case, we set a state variable socket . on ( "outgoing data" , data => this . setState ({ response : data . num })); } render () { const { response } = this . state ; return ( < div style= { { textAlign : "center" } } > < ReactSpeedometer maxValue= { 140 } value= { response } needleColor= "black" startColor= "orange" segments= { 10 } endColor= "red" needleTransition= { "easeElastic" } ringWidth= { 30 } textColor= { "red" } /> </ div > ) } } export default App ;

state.response will hold the value coming from the backend and state.endpoint is just where the server is located. The magic happens in the lifecycle function componentDidMount() . For those of you unfamiliar with React, this function is called when the component is added to the DOM. Hence, this is where well connect to the socket and listen for data.

const socket = socketIOClient(endpoint); simply connects us to the server and opens up a socket connection.

socket.on("outgoing data", data => this.setState({response: data.num})); looks familiar doesn't it? We begin listing on the outgoing data namespace. We have a callback which then takes the response and sets the state to the new value.

Lets take a look at the render function:

render () { const { response } = this . state ; return ( < div style= { { textAlign : "center" } } > < ReactSpeedometer maxValue= { 140 } value= { response } needleColor= "black" startColor= "orange" segments= { 10 } endColor= "red" needleTransition= { "easeElastic" } ringWidth= { 30 } textColor= { "red" } /> </ div > ) }

The ReactSpeedometer component has a bunch of props you can pass to it to customize it. Most of it is self-explanatory but you can read all about it here. I used the needleTransition "easeElastic" because it looks cool but "eastLinear" is probably a better choice for a speedometer. Read about the transition effects here.

The render function extracts the current state value for the speed and pass it into the ReactSpeedometer prop named value . This will update the speedometer.

So how does it look!

https://i.imgur.com/D4qzm7o.gif

(Having issues embedding the gif in this post. Sorry!)

It ended up behaving more like a tachometer but turned out pretty cool!