Node JS is a popular server-side JavaScript runtime environment with an event-driven, non-blocking I/O model. This approach allows Node JS to efficiently handle many concurrent connections without consuming excessive resources.
This is how the blocking and synchronous model works. In this, each operation is executed sequentially, causing the program to wait for each task to complete before moving on to the next. This can lead to inefficiencies and delays, especially when dealing with tasks that require significant processing time.
Now, let's explore how non-blocking or asynchronous works.
The event-driven is a programming model in which programs respond to external events. Instead of following a traditional flow of execution, where the code sequentially executes each statement, Node JS utilizes event-driven architecture. It relies on events, which can be anything from HTTP requests, file system operations, timers, or custom events triggered by the application. These events are registered with associated callbacks, and when an event occurs, the corresponding callback is executed asynchronously.
Traditional I/O operations, such as reading from files or making network requests, are often blocked, which stops the program’s execution until the operation is completed. In contrast, Node JS employs non-blocking I/O, where the execution of the program continues without waiting for the I/O operation to finish. When the operation is completed, a callback is triggered to handle the result.
Scalability: Node JS can handle many concurrent connections efficiently due to its non-blocking nature, making it suitable for building high-performance applications.
Responsiveness: The event-driven model allows Node JS to respond quickly to incoming events, enhancing the responsiveness of applications.
Resource efficiency: By not blocking the event loop, Node JS can better use system resources, reducing the overall memory footprint and increasing overall throughput.
To better know how Node JS manages event-driven, non-blocking I/O, let’s take a closer look at the event loop.
Node JS operates within a single thread and utilizes the event loop to handle events and execute callbacks. The event loop is the core of the event-driven architecture, and it continuously checks for pending events in a loop.
When a new event is registered, it is added to the event queue. The event loop picks up these events from the queue and executes their corresponding callbacks individually. If a callback takes time to complete, it won’t block the entire program, instead, other events can be processed in the meantime.
Let us now see some examples to better understand the event-driven, non-blocking I/O model in Node JS
To better understand this model, let's create a simple real-time chat application project that demonstrates its concepts. The application will allow users to join chat rooms and exchange messages in real time. Follow the steps given below to create the project.
Your project structure should be like this
Create a new folder for the project and navigate into it in the terminal. Run the following command to initialize a new Node JS project and create a package.json file:
npm init -y
We will need the express
framework to set up a simple HTTP server and ws
(WebSocket) library for handling real-time communication. Install them using npm:
npm install express ws
Create a file named server.js
in the project folder and add the following code:
const express = require('express');const http = require('http');const WebSocket = require('ws');const app = express();// Middleware to serve static files from the "public" folderapp.use(express.static('public'));const server = http.createServer(app);// WebSocket serverconst wss = new WebSocket.Server({ server });wss.on('connection', (ws) => {console.log('WebSocket client connected');ws.on('message', (message) => {const data = JSON.parse(message); // Parse the received string back to an objectconsole.log('Received message:', data);// Broadcast the message to all connected clientswss.clients.forEach((client) => {if (client !== ws && client.readyState === WebSocket.OPEN) {client.send(JSON.stringify(data)); // Convert the data back to a string before sending}});});ws.on('close', () => {console.log('WebSocket client disconnected');});});server.listen(3000, () => {console.log('Server is running');});
Create an HTML file named index.html
in the project folder:
<!DOCTYPE html><html><head><title>Real-Time Chat</title></head><body><h1>Real-Time Chat</h1><input type="text" id="messageInput" placeholder="Type your message"><button onclick="sendMessage()">Send</button><ul id="messageList"></ul><script>const socket = new WebSocket('ws://localhost:3000');socket.onmessage = (event) => {const messageList = document.getElementById('messageList');const li = document.createElement('li');li.textContent = event.data;messageList.appendChild(li);};function sendMessage() {const messageInput = document.getElementById('messageInput');const message = messageInput.value;socket.send(JSON.stringify(message)); // Convert message to string using JSON.stringify()messageInput.value = '';}</script></body></html>
Run the server by executing the following command in the terminal:
node server.js
In this project, we used the event-driven, non-blocking I/O model to create a real-time chat application. The WebSocket.Server
provided by the ws
library enables real-time communication between clients and the server. The server listens for incoming WebSocket connections and registers event listeners for the connection
, message
, and close
events.
When a new client connects, the connection
event is triggered, and we log a message to the console. When a message is received from a client (message
event), we log it and then broadcast it to all connected clients using the send
method of the WebSocket objects. The server does not wait for messages to be sent to all clients, instead, it processes incoming messages asynchronously, allowing it to handle multiple clients simultaneously.
In the frontend HTML file, we use JavaScript to create a WebSocket connection to the server. When the user sends a message, it is sent to the server using the WebSocket connection without blocking the main UI thread. When the server returns a message, the message
event listener updates the UI with the received message in real-time.
Here is an example of an asynchronous file read.
It is a long established fact that a readerwill be distracted by the readable contentof a page when looking at its layout.The point of using Lorem Ipsum is that it has amore-or-less normal distribution of letters, as opposed to using'Content here, content here', making it looklike readable English. Many desktop publishing packages andweb page editors now use Lorem Ipsum as their default model text,and a search for 'lorem ipsum' will uncover many web sites still intheir infancy. Various versions have evolved over the years,sometimes by accident, sometimes on purpose(injected humour and the like).
In this example, the fs.readFile
function reads the content of the example.txt
asynchronously. While the file is being read, the program executes the next line, which logs “File reading started” to the console. When the file reading is completed, the callback function is executed to log the contents of the file.
Let's explore one more example for our better understanding.
const http = require('http'); const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello, World!'); }); server.listen(3000, () => { console.log('Server is running'); });
This code sets up a basic HTTP server using the http
module. When a request is made to this server, it responds with Hello, World!
without blocking the event loop.
Node JS event-driven, non-blocking I/O model is a powerful approach to building scalable, high-performance applications. By using asynchronous programming, Node JS can efficiently handle multiple connections simultaneously without consuming excessive resources.