The Power of WebSockets
WebSockets are a super-powerful technology that has changed the way we use the internet today. They are ubiquitous; we’ve all used web sockets in some way or another. In fact, you’re probably be using one right now.
What are WebSockets?
Imagine a world where every time someone messaged you, you had to refresh the page to view their message or, as you’re watching GameStop stock go red, you would have to refresh to get the most up-to-date price. How would you be able to cry in real-time? That’s where WebSockets come in. They allow for low-latency bi-directional communication between client and server. What does this mean? Very simply, a WebSocket is a way for your device to make a connection to a server (where the valuable data is), and then you can get fed data in realtime without needing to refresh your page to get the data again.
HTTP vs WebSockets
In general, most websites use Rest API to fetch data from servers that use the HTTP protocol. The client makes a request, and the server responds with the (hopefully) correct data. But this is strictly unidirectional. The server cannot send any data to the client unless the client has requested it first. Already, we can see the limitations to this. With something like a sports game score, if your favorite team were to score a point, the server wouldn’t be able to send you the updated score until you made another request, usually by refreshing the page.
WebSockets build off of HTTP protocol in that it uses HTTP initially to establish the connection but keeps the TCP connection alive so the client and server can continue sending messages with each other. This allows for the “real-time” updates you see on many websites nowadays. Once both parties agree that the connection should be closed, the TCP connection will be torn down, and the WebSocket will then be closed.
Why WebSockets?
WebSocket isn’t the only protocol that allows for bidirectional communication. Both long-polling and server-sent events (SSE) achieve this same goal but each has its own specific use cases, so WebSockets is still a great technology to learn. With long-polling, it only allows for unidirectional communication one at a time, which leads to latency issues at times and is much more resource-intensive than WebSockets. SSE allows the server to send data to the client without needing the client to request it first. However, the issue with this is that it isn’t completely supported by all browsers, namely IE and Edge, and can be harder to implement than WebSockets. So, in general, WebSockets is a quick and easy way to set bidirectional communication between server and client for smaller side projects.How Do We Use Socket.io
If that sounded daunting, no worries. Almost all of these concepts are abstracted out by socket.io to make it very easy for us. All it takes is setting up an endpoint in our Express server and instantiating an instance of the io object in our React frontend establishing a connection. While this may seem complicated now, the implementation in code is easier to understand. We can use emit
, which is used for sending data and on
to receive data. Emitting from the client will send the data to the server, and emitting from the server will send the data to all client instances. on
can be used on either side to receive data from either side.
Let’s Try It!
Today, we’ll be building a Node.js/Express server that uses socket.io to create a real-time chat application. I know, I know, every WebSocket tutorial uses this same idea for a project, but it really is the simplest way to quickly understand how WebSockets work. Plus, if you already know how to use WebSockets, chances are you’re not reading this article.
Here are some good (but not necessarily required) things to know before getting started:
- React/HTML/CSS
- Node.js/Express
- Basic understanding of how full-stack applications work
- Basic terminal commands
Here are some things you’re going to need to be installed:
- Node.js
- npm
- yarn (optional, if you know you know)
- create-react-app
- Some text editor or IDE, VSCode preferred
Boilerplate Code
Setting up the project is boring for some people. If you want to skip it, just clone this repo I made at https://github.com/mxinburritos/full-stack-boilerplate and go to the next section. Otherwise, let’s get started.
First, let’s go to the terminal and change the directory into your folder with all of your projects and let’s create a new folder that’s named to whatever we want to call the project. Then, change the directory into that folder and then use create-react-app to create our frontend.
mkdir web-socket-chat
cd web-socket-chat
npx create-react-app client OR yarn create react-app client
Then, we’ll make the backend for our project using Express. From here on out, I will be using only yarn commands but use the npm equivalents if you’re using npm. Here, I’m creating the directory for the server and creating a package.json
. We'll be installing Express, socket.io, and a few other libraries we will need.
mkdir server
cd server
yarn init -yes
yarn add express socket.io cors http
touch server.js
Now, for some boilerplate code for Express. This code should all go in your server.js
file.
const app = require('express')();
const server = require('http').Server(app);
const io = require('socket.io')(server, {
cors: {
origin: '<http://localhost:3000>',
methods: ['GET', 'POST']
}
});
const PORT = process.env.PORT || 5000;io.on('connection', (socket) => {
console.log('a user connected');
})server.listen(PORT, () => {
console.log(`listening on port ${PORT}`);
})
This looks like a ton, but if you know Express, most of it should look familiar. The code about the cors will help us bypass a security measure that you can read more about here: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors. We’re also creating the socket.io object and setting up an event listener for ‘connection’ that we will use later.
For our frontend, this code should go into the App.js
file.
import './App.css';
import io from 'socket.io-client';const socket = io('<http://localhost:5000>')function App() {
return (
<div className="App">
App
</div>
);
}export default App;
The main thing to note is the line calling the io method. We’re passing in the backend URL, which will establish our connection between the front end and backend.
Now that we have all of our boilerplate code for both our frontend and backend, we can now try running both. First, we can change directory into our client folder and then use the start command.
cd ../client
yarn add socket.io-client
yarn start
That will start the development server, which will show up in the browser at localhost:3000
. For the backend, we will need to install an extra package. Open up another terminal window and change directory into the server
folder. We will be installing a package called nodemon, which will reload the server every time we make a change to anything in the backend.
yarn add -D nodemon
The -D
flag ensures that it will be installed as a development dependency. The last thing to do is to add this into your package.json
that's located in your /server
folder.
"scripts": {
"start": "nodemon server.js"
},
At this point, we’re finally done setting up our environment, and now we can just run yarn start
in the server
to start it as well.
With the boilerplate code I gave for server.js, we now have a connection event that will run every time a new connection is made. If you’re now running both and all of the code is correct, you should now be able to refresh the browser running http://localhost:3000 and you’ll see a user connected in the terminal each time. That means that the web socket connection has been made and we can really get started.
Testing Out socket.io Connection
Again, The first thing we want to do is to establish the connection and make sure everything is working. With the boilerplate code I gave for server.js
, we now have a connection event that will run every time a new connection is made. If you're now running both and all of the code is correct, you should now be able to refresh the browser running http://localhost:3000
, and you'll see a user connected
in the terminal each time. That means that the web socket connection has been made, and we can really get started.
Basic socket.io concepts
Before we implement the socket.io stuff now, let’s go over a few concepts in the library:
- emit — emitting an event will send data to the backend. When we do, we can pass in the event name and the data we want to send.
- on — this is a listener listening for a certain event type to be emitted. We pass in the event name we are listening for and a callback function that will have the data being received as the parameter.
Together, these will both help us send data back and forth in realtime.
Finishing the Frontend
We’ll be creating a super barebones frontend with no styling but I encourage you to make it look pretty! I’ll be showing each code snippet and then explaining what it does right after.
const [message, setMessage] = useState('');
const [messages, setMessages] = useState([]);
Here, we’re just creating the state variables. Message will be the string that holds the message in the textbox, which we will add next. Messages will be an array of all messages that have been sent and they will be displayed on the screen.
useEffect(() => {
socket.on('chat message', (msg) => {
const newArray = messages.concat(msg)
setMessages(newArray);
})
}, [messages])
We’re using the useEffect hook with a socket event listener for the event chat message. We’ll be setting up that event soon, but the msg parameter will be the message that has been sent. It takes that and adds it to the message’s state. Be sure to add messages as a dependency; otherwise, the message will be added extra times (which we don’t want).
const handleSend = () => {
socket.emit('chat message', message);
}
This is a super simple method that emits the event chat message which sends the message state. This event will go to the backend server where we will then handle it. In the next code snippet, we will have this method be called whenever the button is pressed.
return (
<div className="App">
<input value={message} onChange={(e) => {setMessage(e.target.value)}}></input>
<button onClick={handleSend}>Send</button>
{messages.map((msg, i) => {
return <p key={`${msg} ${i}`}>{msg}</p>
})}
</div>
);
When I said barebones, I meant barebones. All we have here is an input and a button. Below it, it iterates through the array store in the messages state and displays a paragraph for each string. That’s how the messages are displayed. There are a few important things to note. First, I make the input a controlled component. Inputs have their own state (the value in the textbox), which isn’t ideal because we don’t want to manage two separate states. To make the React state and the input state the same, we set the input’s value to the message state and then change the message state whenever the input is changed. This seems redundant, but it ensures that the input’s value will always be the same as the React state. This means we can change or get the value from somewhere else in the component. For this project, it’s important we can get the value because that value will be the string we are sending. Lastly, we have a button that will call the handleSend method, which will emit the chat message event and the message state. And remember that the message state is the same as the input state, so that means we’re emitting whatever is in the input box.
Finally, this is what all of the frontend code should look like:
import React, { useEffect, useState } from 'react';
import './App.css';
import io from 'socket.io-client';const socket = io('<http://localhost:5000>')function App() {
const [message, setMessage] = useState('');
const [messages, setMessages] = useState([]); useEffect(() => {
socket.on('chat message', (msg) => {
const newArray = messages.concat(msg)
setMessages(newArray);
})
}, [messages]) const handleSend = () => {
socket.emit('chat message', message);
} return (
<div className="App">
<input value={message} onChange={(e) => {setMessage(e.target.value)}}></input>
<button onClick={handleSend}>Send</button>
{messages.map((msg, i) => {
return <p key={`${msg} ${i}`}>{msg}</p>
})}
</div>
);
}export default App;
That’s all for the frontend!
Backend?
If you thought the frontend was a lot, then you’ll be glad to hear that the backend is only 5 whole lines. Seriously.
io.on('connection', (socket) => {
socket.on('chat message', (msg) => {
io.emit('chat message', msg);
})
})
Actually, if you used the boilerplate I gave before, then you would’ve deleted a line and added 3 lines, so it’s really just 2 lines. All it’s doing is listening for the chat message event, emitted every time the button is clicked. We receive the string as the msg parameter, which we then take and emit back with another chat message event. This will then be received by every client and added it to the messages state array and displaying it. Why are we emitting the chat message again? It makes it so that whenever one client sends a message, the server will take it and deliver it to all other clients. That means that any number of clients can be connected, and when one sends a message, all others will receive it.
Here’s what all of the backend code looks like:
const app = require('express')();
const server = require('http').Server(app);
const io = require('socket.io')(server, {
cors: {
origin: '<http://localhost:3000>',
methods: ['GET', 'POST']
}
});
const PORT = process.env.PORT || 5000;io.on('connection', (socket) => {
socket.on('chat message', (msg) => {
io.emit('chat message', msg);
})
})server.listen(PORT, () => {
console.log(`listening on port ${PORT}`);
})
Now, hopefully, everything went correctly and you should be able to open two tabs to [<http://localhost:3000>](<http://localhost:3000>)
which you can send messages and have both tabs display them.
If you want to see the final project, go check out my repo: https://github.com/mxinburritos/socket-io-demo
The Future of WebSockets
In just the past week, a new paradigm of development with WebSockets was written in this article talking about the idea of HTML-over-WebSockets. All of the cool new web development concepts like fast prototyping and deployment, server-side state management, single page PWA, and good search engine optimization are all bundled under this concept which could make WebSockets even more relevant than they are today. By using HTML-over-WebSockets, small changes with the database can update the client-side immediately. Form validation can be done server-side. You can just have a bit of HTML on the client-side and have almost all of the logic on the server. Because of the low-latency nature of web sockets, this would make performance much better and easier to manage! Not only are they cool, but WebSockets could be a big part of the future of web development.
What Now?
I hope that this project shows how powerful web sockets are and how simple they are to use. I mean, you should’ve only added 5 lines for the backend. Regardless, I hope you take these concepts and create some amazing web socket apps with them. Some ideas are a shared text editor, painting tool, or a more advanced chatting application with rooms and/or authentication. Go on and having fun programming!
Questions and Concerns
This is my first technical article and I have no doubt that some things can be better clarified! I urge you send any suggestions or questions to my email: mxin@gatech.edu.