Socket-io Intro

Project#1 — Socket_series — Episode #0

J3
Jungletronics
14 min readSep 20, 2023

--

[update to Socket.IO 4.x @ Jul 2024]

No prior familiarity with Node.js or Socket.IO?

No worries! This post is suitable for individuals of all skill levels who are interested in learning through hands-on experience.

In this post:

We will Compose a chat message
using Socket.io.

Welcome aboard!

The concept around the chat app is that the server will receive a message and distribute it to all other connected clients.

Developing a chat message system (or any real-time system) usually involves the repetitive task of polling the server for updates, handling timestamps, and frequently leads to communication being slower than expected.

Sockets represent the ideal solution for most real-time systems.

We are currently following the official socket.io tutorial.
For additional details, please visit that location.

Let’s get it on!

My System:

OS: Ubuntu 24.04 LTS
Processor: Intel® Core™ i7–9750H × 12 — 8.0 GiB RAM

Project initialization

0#step — Install Node.js

Make sure you have Node.js installed on your system. If not, please install it from the official Node.js website.

Ubuntu terminal:

sudo apt install npm

Next, create the directory structure and launch VSCode:

mkdir socket
cd socket
mkdir proj_01
cd proj_01
code .

1#Step — Create a file package.json

npm init

Update package.json:

  • Modify the name, version, description, type and main fields.

socket/proj_01/package.json

{
"name": "socket-chat-example",
"version": "0.0.1",
"description": "my first socket.io app",
"type": "module",
"main": "server.js",
...
},
}

2#step — Then install nodemon:

npm i --save-dev nodemon

Nodemon is a tool that helps develop Node.js based applications by automatically restarting the node application when file changes in the directory are detected. Install it as a development dependency.

3#step — Setup package.json Scripts

Open your package.json file and modify the following scripts:

server.js

"scripts": {
"devStart": "nodemon server.js"
},

4#step — In the main directory of your project, on Terminal type:

proj_01/

npm install express@4

The command npm install express@4 is used to install a specific version of the Express.js framework using the Node Package Manager (npm). Let's break down what this command does:

  1. npm: npm stands for Node Package Manager, and it is a package manager for Node.js and JavaScript. It is used to manage dependencies (libraries and packages) in a Node.js project.
  2. install: This is an npm command used to install packages or dependencies into your project. When you run npm install, npm looks in your project's package.json file to determine which packages to install and their versions.
  3. express@4: This part of the command specifies the package to be installed and its version. In this case, it's installing a package named "express" with version 4. By specifying @4, you are explicitly requesting version 4 of the Express.js framework. Different versions of packages may have different features or APIs, so specifying the version ensures that your project uses a specific version with known behavior.

So, when you run npm install express@4, it will fetch and install version 4 of the Express.js framework into your project, and it will be listed as a dependency in your project's package.json file. This allows you to use Express.js in your Node.js application and leverage its features for building web applications.

5#step — Let us set up our server application by creating :

proj_01/server.js

import express from 'express'
import { createServer } from 'node:http'

const app = express()
const server = createServer(app)

app.get('/', (req, res) => {
res.send('<h1>Hello Socket.io!</h1>')
})

server.listen(3000, () => {
console.log('server running at http://localhost:3000')
})

The code provided is a basic example of how to set up a Node.js application using the Express.js framework and create an HTTP server using the built-in node:http module. Let’s break down what this code does:

  • Importing Modules:

import express from ‘express’: This line imports the Express.js framework as a module. It uses ES6 module syntax, assuming your project is set up to support ES6 modules. If you’re using an older version of Node.js or CommonJS modules, you might see require(‘express’) instead.

import { createServer } from ‘node:http’: This line imports the createServer function from the built-in node:http module. It’s used to create an HTTP server.

  • Setting up Express:

const app = express(): This code initializes an instance of the Express application, which will be used to define routes and handle HTTP requests.

  • Defining a Route:

app.get(‘/’, (req, res) => {…}): This code defines a route for handling HTTP GET requests to the root URL (/). When a user accesses the root URL of your application, the function provided as the second argument is executed. In this case, it sends back an HTML response with the text Hello Socket.io! as an <h1> element.

  • Creating an HTTP Server:

const server = createServer(app): This line creates an HTTP server using the createServer function from the node:http module. The app instance of Express is passed as an argument to the server, which means that Express will handle incoming HTTP requests.

  • Starting the Server:

server.listen(3000, () => {…}): This code starts the HTTP server and makes it listen on port 3000. When the server starts successfully, the callback function is executed, and it logs a message to the console indicating that the server is running.

In summary, this code sets up a basic Express.js application, defines a single route to handle requests to the root URL, and creates an HTTP server that listens on port 3000. When you access http://localhost:3000 in your browser, you should see the Hello Socket.io! message displayed in an <h1> element as a response from your server.

6#step — Now start Nodemon:

npm run devStart

After saving this file, open your browser and navigate to http://localhost:3000. You will see Hello Socket.io! rendered on the page.

Serving HTML

7#step — To simplify the process and avoid the confusion of passing individual HTML elements, consider passing the entire file instead.

Let’s refactor our route handler to use sendFile() method instead.

GoTo proj_01/server.js an and change this:

app.get('/', (req, res) => {
res.send('<h1>Hello Socket.io!</h1>')
})

To this:

import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'

....

const __dirname = dirname(fileURLToPath(import.meta.url))

app.get('/', (req, res) => {
res.sendFile(join(__dirname, 'index.html'))
})

This setup allows your server to serve the index.html file when the root URL is accessed, and it uses ES module syntax instead of CommonJS.

Let's break down the changes and how the code works:

Importing Required Modules

import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
  • fileURLToPath: A function from the node:url module that converts a file URL to a file path.
  • dirname: A function from the node:path module that returns the directory name of a path.
  • join: A function from the node:path module that joins multiple path segments into a single path.

Calculating __dirname

const __dirname = dirname(fileURLToPath(import.meta.url))
  • import.meta.url: Provides the URL of the current module. In the context of ES modules, it returns the URL as a string.
  • fileURLToPath(import.meta.url): Converts the module URL to a file path.
  • dirname(fileURLToPath(import.meta.url)): Gets the directory name of the file path, effectively mimicking the behavior of __dirname in CommonJS modules.

Setting Up the Route Handler

8#step — Create index.html file:

proj_01/index.html

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Socket.IO chat</title>
<style>
body { margin: 0; padding-bottom: 3rem; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }

#form { background: rgba(0, 0, 0, 0.15); padding: 0.25rem; position: fixed; bottom: 0; left: 0; right: 0; display: flex; height: 3rem; box-sizing: border-box; backdrop-filter: blur(10px); }
#input { border: none; padding: 0 1rem; flex-grow: 1; border-radius: 2rem; margin: 0.25rem; }
#input:focus { outline: none; }
#form > button { background: #333; border: none; padding: 0 1rem; margin: 0.25rem; border-radius: 3px; outline: none; color: #fff; }

#messages { list-style-type: none; margin: 0; padding: 0; }
#messages > li { padding: 0.5rem 1rem; }
#messages > li:nth-child(odd) { background: #efefef; }
</style>
</head>
<body>
<ul id="messages"></ul>
<form id="form" action="">
<input id="input" autocomplete="off" /><button>Send</button>
</form>
</body>
</html>

If you start the process (by running npm run devStart) and browse the page it should look like this:

At this juncture, when you input something, nothing occurs. Let’s address this issue!

Integrating Socket.IO

9#step — Incorporating Socket.IO:

Socket.IO consists of two components:

. server component that seamlessly integrates with or attaches itself to the Node.js HTTP Server (provided by the socket.io package).

. client library that is loaded on the browser side (utilized via the socket.io-client package).

During the development process, Socket.IO automatically serves the client-side library for us, simplifying the setup. Thus, at this point, we only need to install one module and re-run Nodemon:

ctrl+c
npm install socket.io

This command installs the necessary module and adds it as a dependency in the package.json file.

Now re-start Nodemon:

npm run devStart

Now, let's proceed to modify server.js to incorporate Socket.IO functionality.

Refactoring the proj_01/server.js:

import { Server } from 'socket.io'
const io = new Server(server)
io.on('connection', (socket) => {
console.log(`Socket_ID: ${socket.id} has joined our Server!`)
socket.on('disconnect', ()=>{
console.log(`Socker_ID: ${socket.id} has disconected from our Server!`)
})
})

It sets up a Socket.IO server instance, listens for incoming socket connections, and logs a message when a user connects to the server.

Here’s an explanation of what each part of the code does:

  • Event Listener: io.on('connection', ...) sets up a listener for new socket connections.
  • Socket Object: When a client connects, a socket object is passed to the callback function.
  • Logging Connection: console.log(...) logs the unique ID of the connected socket.
  • Disconnection Event: socket.on('disconnect', ...) sets up a listener for when the socket disconnects.
  • Logging Disconnection: console.log(...) logs the unique ID of the disconnected socket.
  • Purpose: The code helps track when clients connect and disconnect by logging their socket IDs.
  • Socket ID: The socket.id provides a unique identifier for each connection.
  • Real-time Communication: This is part of setting up real-time communication using Socket.IO.
  • Connection Lifecycle: It manages and logs the lifecycle of each socket connection.
  • Debugging: Useful for monitoring and debugging socket connections in the application.

10#step — In the index.html file, insert the following code snippet just before the closing </body> tag. This action will install the module and include the dependency in package.json.

proj_01/index.html

<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io()
</script>

The code provided in your HTML file is used to include the Socket.io client library and create a WebSocket connection to your server. Let's break down what these lines of code do:

  1. <script src="/socket.io/socket.io.js"></script>:
  • This line includes the Socket.io client library in your HTML document. It specifies the src attribute as /socket.io/socket.io.js, which is a URL relative to the root of your web server.
  • The socket.io.js script is provided by the Socket.io server and is responsible for establishing and managing WebSocket connections between the client and the server.

2. <script> const socket = io() </script>:

  • This script block creates a WebSocket connection to the server using the io() function, which is made available by the Socket.io client library.
  • When this script runs in a web browser, it initializes a WebSocket connection to the server where your Socket.io server is running.
  • The socket object that is created represents the WebSocket connection, and you can use it to send and receive real-time messages and events between the client and server using Socket.io.

In summary, these lines of code in your HTML file load the Socket.io client library and establish a WebSocket connection to your server when a user loads the webpage. This connection allows for real-time communication between the client and server, making it possible to send and receive data, events, and messages in a WebSocket-based, bidirectional manner.

11#step — If you restart the process and subsequently refresh the webpage (then close), you will observe the console displaying the messages:

Socket_ID: QhMDFfEHD0ppycg8AAAB has joined our Server!
Socker_ID: QhMDFfEHD0ppycg8AAAB has disconected from our Server!

If you open multiple tabs, you will notice that multiple messages are displayed.

Emitting events

The main idea behind Socket.IO is that you can send and receive any events you want, with any data you want. Any objects that can be encoded as JSON will do, and binary data is supported too.

12#step — Emitting Events

For instance, we can configure it in such a way that when a user enters a message, the server receives it as an event labeled chat message. This results in the following updated script section in index.html:

proj_01/index.html

<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io()

const form = document.getElementById('form')
const input = document.getElementById('input')

form.addEventListener('submit', (e) => {
e.preventDefault()
if (input.value) {
socket.emit('chat message', input.value)
input.value = ''
}
})
</script>

The JavaScript code provided adds an event listener to a form element with the id form and listens for the submit event. When the form is submitted, it prevents the default form submission behavior, checks if the input field has a value, and if so, it emits a chat message event with the input value using the socket object (a Socket.io socket).

Here’s a breakdown of what this code does:
1 . const form = document.getElementById(‘form’):

This line selects an HTML form element with the id form and assigns it to the form variable.

2 . const input = document.getElementById(‘input’):

This line selects an HTML input element with the id input and assigns it to the input variable.

3 . form.addEventListener(‘submit’, (e) => {…}):

This code adds an event listener to the submit event of the form element.
When the form is submitted (e.g., when a user presses Enter or clicks a submit button inside the form), the provided callback function is executed.

4 . e.preventDefault():

This line prevents the default form submission behavior, which would normally cause the browser to refresh the page or navigate to a new URL.

5 . if (input.value) { … }:

This checks whether the input field has a non-empty value.

6 . socket.emit(‘chat message’, input.value):

If the input field has a value, it emits a chat message event to the server using the socket object. The event name is chat message, and the input value is sent as the data associated with the event.

7. input.value = ‘’:

After sending the message, it clears the value of the input field, making it ready for the user to type a new message.

In the context of a real-time chat application, this code allows users to submit chat messages by filling out a form and sending them to the server via WebSocket. The server can then broadcast the message to other connected clients, enabling real-time chat functionality. The actual handling of the chat message event on the server and the broadcasting of messages to other clients would need to be implemented on the server side.

13#step —In the server.js file, we log the chat message event rather than logging the connection.

proj_01/server.js

io.on('connection', (socket) => {
socket.on('chat message', (msg) => {
console.log('<io># message: ' + msg)
})
})

Type some text in the form, and it will be logged to the console:

<io># message: dasda

Broadcasting

The next goal is for us to emit the event from the server to the rest of the users.

14#step — Broadcasting:

Our next objective is to propagate the event from the server to all other users. To accomplish this, Socket.IO provides us with a io.emit() method, which allows us to send an event to every connected user.

proj_01/server.js

io.on('connection', (socket) => {
socket.on('chat message', (msg) => {
io.emit('chat message', msg)
})
})

The code provided is a modification of the server-side logic for handling incoming chat messages in a Socket.io application. In this updated code, when a client sends a chat message, the server broadcasts the message to all connected clients, including the sender. Let's break down what this code does:

  1. io.on('connection', (socket) => {...}): This code sets up an event listener for the connection event on the Socket.io server, just like before.
  2. (socket) => {...}: Inside the connection callback function, another event listener is set up on the socket object for each connected client.
  3. socket.on('chat message', (msg) => {...}): This event listener listens for the chat message event emitted by clients when they send chat messages, just like before.
  4. (msg) => {...}: The callback function for the chat message event takes one parameter, msg, which represents the message sent by the client.
  5. io.emit('chat message', msg): Inside the chat message event handler, this line uses the io object to emit the received message (msg) to all connected clients.
  6. io.emit broadcasts the message to all connected clients, including the client that sent the original message. This allows all clients to see the chat messages in real-time.

With this modification, when a client sends a chat message, the server sends that message to all connected clients, enabling a real-time chat experience where messages are visible to everyone in the chat room. This is a common pattern for building real-time chat applications using Socket.io.

15#step — On the client side, when we receive a chat message event, we’ll incorporate it into the webpage. Add before </script> to:

proj_01/index.html

<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io()

const form = document.getElementById('form')
const input = document.getElementById('input')
const messages = document.getElementById('messages')

form.addEventListener('submit', (e) => {
e.preventDefault()
if (input.value) {
socket.emit('chat message', input.value)
input.value = ''
}
})

socket.on('chat message', (msg) => {
const item = document.createElement('li')
item.textContent = msg
messages.appendChild(item)
window.scrollTo(0, document.body.scrollHeight)
})
</script>

Here are the final versions:

proj_01/index.html :

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Socket.IO chat</title>
<style>
body { margin: 0; padding-bottom: 3rem; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }

#form { background: rgba(0, 0, 0, 0.15); padding: 0.25rem; position: fixed; bottom: 0; left: 0; right: 0; display: flex; height: 3rem; box-sizing: border-box; backdrop-filter: blur(10px); }
#input { border: none; padding: 0 1rem; flex-grow: 1; border-radius: 2rem; margin: 0.25rem; }
#input:focus { outline: none; }
#form > button { background: #333; border: none; padding: 0 1rem; margin: 0.25rem; border-radius: 3px; outline: none; color: #fff; }

#messages { list-style-type: none; margin: 0; padding: 0; }
#messages > li { padding: 0.5rem 1rem; }
#messages > li:nth-child(odd) { background: #efefef; }
</style>
</head>
<body>
<ul id="messages"></ul>
<form id="form" action="">
<input id="input" autocomplete="off" /><button>Send</button>
</form>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io()

const form = document.getElementById('form')
const input = document.getElementById('input')
const messages = document.getElementById('messages')

form.addEventListener('submit', (e) => {
e.preventDefault()
if (input.value) {
socket.emit('chat message', input.value)
input.value = ''
}
})

socket.on('chat message', (msg) => {
const item = document.createElement('li')
item.textContent = msg
messages.appendChild(item)
window.scrollTo(0, document.body.scrollHeight)
})

</script>
</body>
</html>

proj_01/server.js :

import express from 'express'
import { createServer } from 'node:http'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
import { Server } from 'socket.io'

const app = express()
const server = createServer(app)
const io = new Server(server)

const __dirname = dirname(fileURLToPath(import.meta.url))

app.get('/', (req, res) => {
res.sendFile(join(__dirname, 'index.html'))
})

// io.on('connection', (socket) => {
// console.log(`Socket_ID: ${socket.id} has joined our Server!`)
// socket.on('disconnect', ()=>{
// console.log(`Socker_ID: ${socket.id} has disconected from our Server!`)
// })
// })

// io.on('connection', (socket) => {
// socket.on('chat message', (msg) => {
// console.log('message: ' + msg)
// })
// })

io.on('connection', (socket) => {
socket.on('chat message', (msg) => {
io.emit('chat message', msg)
})
})

server.listen(3000, () => {
console.log('server running at http://localhost:3000')
})
After each update, we make it a practice to annotate the code and position the fresh code above the existing one. This serves as a prompt for you to engage in a review and study process.

16#step — Suggestions for Enhancing the Application:

  1. Implement a feature that informs all connected users when someone joins or leaves the chat.
  2. Enable the use of user-defined nicknames for a more personalized experience.
  3. Optimize message handling by instantly displaying sent messages for the sender without retransmitting them.
  4. Introduce a feature that indicates when a user is actively typing.
  5. Display a list of online users for greater visibility.
  6. Incorporate a private messaging feature for one-on-one conversations.
  7. Feel free to share any enhancements you make with the community!

👉️GitHub

Credits & References

Socket.IOGetting started

Engine.IO: the realtime engine — bi-directional communication layer for Socket.IO

Want to make a chat app? Get Real-time With WebSockets & Socket.io! by Good Moning developers!

Related Posts

07#kidSeries — MIT Meets Express Socket-io & J5 — The Bleeding Edge Technology Debut

21#arduSeries— Making Ideas Shine with Johnny-Five!! — Get started w/ Johnny-Five on Arduino

--

--

J3
Jungletronics

😎 Gilberto Oliveira Jr | 🖥️ Computer Engineer | 🐍 Python | 🧩 C | 💎 Rails | 🤖 AI & IoT | ✍️