What is the event-driven, non-blocking I/O model in Node JS?

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.

Blocking or Synchronous
Blocking or Synchronous

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.

Event-driven

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.

Non-blocking I/O

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.

How Node JS overcome the problem of blocking of I/O operations.
How Node JS overcome the problem of blocking of I/O operations.

Advantages of event-driven, non-blocking I/O

  1. Scalability: Node JS can handle many concurrent connections efficiently due to its non-blocking nature, making it suitable for building high-performance applications.

  2. Responsiveness: The event-driven model allows Node JS to respond quickly to incoming events, enhancing the responsiveness of applications.

  3. Resource efficiency: By not blocking the event loop, Node JS can better use system resources, reducing the overall memory footprint and increasing overall throughput.

Event loop

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.

Event loop
Event loop

Examples

Let us now see some examples to better understand the event-driven, non-blocking I/O model in Node JS

Example 1: Real-time chat application

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.

Project structure

Your project structure should be like this

widget

Step 1: Set up the project

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

Step 2: Install dependencies

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

Step 3: Create the server

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" folder
app.use(express.static('public'));
const server = http.createServer(app);
// WebSocket server
const 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 object
console.log('Received message:', data);
// Broadcast the message to all connected clients
wss.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');
});

Step 4: Create the frontend

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>

Step 5: Start the application

Run the server by executing the following command in the terminal:

node server.js

Explanation

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.

Example 2: Asynchronous file read

Here is an example of an asynchronous file read.

index.js
example.txt
It is a long established fact that a reader
will be distracted by the readable content
of a page when looking at its layout.
The point of using Lorem Ipsum is that it has a
more-or-less normal distribution of letters, as opposed to using
'Content here, content here', making it look
like readable English. Many desktop publishing packages and
web page editors now use Lorem Ipsum as their default model text,
and a search for 'lorem ipsum' will uncover many web sites still in
their 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.

Example 3: Asynchronous HTTP Server

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.

Conclusion

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.

Copyright ©2024 Educative, Inc. All rights reserved