DEV Community

Cover image for Real-Time Applications with Socket.IO
Aditya
Aditya Subscriber

Posted on

Real-Time Applications with Socket.IO

Introduction

socket

In today's digital landscape, users expect immediate feedback and instant communication. Whether it's a chat application, collaborative document editing, live notifications, or multiplayer games, real-time applications have become the standard rather than the exception. These applications require bidirectional communication between client and server, where data flows seamlessly in both directions without the traditional request-response cycle.

Implementation

Socket.IO operates on a client-server architecture where both sides can initiate communication. The server maintains persistent connections with multiple clients, allowing it to push data instantly when events occur. This architecture eliminates the need for constant polling and reduces server load while providing immediate updates.

The core concept revolves around events. Instead of traditional HTTP endpoints, Socket.IO uses custom events that both client and server can emit and listen for. This event-driven approach makes the code more intuitive and easier to manage.

Setting Up the Server

First, let's create a basic Node.js server with Socket.IO. You'll need to initialize a new project and install the required dependencies:

npm init -y
npm install express socket.io
Enter fullscreen mode Exit fullscreen mode

Here's a complete server implementation:

// server.js
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const path = require('path');

const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
  cors: {
    origin: "*",
    methods: ["GET", "POST"]
  }
});

// Serve static files
app.use(express.static(path.join(__dirname, 'public')));

// Store connected users
const connectedUsers = new Map();

io.on('connection', (socket) => {
  console.log(`User connected: ${socket.id}`);

  // Handle user joining
  socket.on('user-join', (username) => {
    connectedUsers.set(socket.id, username);
    socket.broadcast.emit('user-connected', {
      username: username,
      userId: socket.id
    });

    // Send current user count
    io.emit('user-count', connectedUsers.size);
  });

  // Handle chat messages
  socket.on('chat-message', (data) => {
    const username = connectedUsers.get(socket.id);
    io.emit('message', {
      username: username,
      message: data.message,
      timestamp: new Date().toISOString()
    });
  });

  // Handle typing indicators
  socket.on('typing', (data) => {
    const username = connectedUsers.get(socket.id);
    socket.broadcast.emit('user-typing', {
      username: username,
      isTyping: data.isTyping
    });
  });

  // Handle disconnection
  socket.on('disconnect', () => {
    const username = connectedUsers.get(socket.id);
    if (username) {
      connectedUsers.delete(socket.id);
      socket.broadcast.emit('user-disconnected', {
        username: username,
        userId: socket.id
      });
      io.emit('user-count', connectedUsers.size);
    }
    console.log(`User disconnected: ${socket.id}`);
  });
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

Setting Up the Client

Create a simple HTML client in a public folder:

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Real-Time Chat with Socket.IO</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
        }

        #messages {
            height: 400px;
            border: 1px solid #ccc;
            overflow-y: scroll;
            padding: 10px;
            margin-bottom: 10px;
        }

        .message {
            margin-bottom: 10px;
            padding: 5px;
            border-radius: 5px;
            background-color: #f5f5f5;
        }

        .username {
            font-weight: bold;
            color: #007bff;
        }

        .timestamp {
            font-size: 0.8em;
            color: #666;
        }

        #messageInput {
            width: 70%;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 4px;
        }

        #sendButton {
            width: 25%;
            padding: 10px;
            background-color: #007bff;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }

        #userInfo {
            margin-bottom: 20px;
            padding: 10px;
            background-color: #e9ecef;
            border-radius: 4px;
        }

        .typing-indicator {
            font-style: italic;
            color: #666;
        }
    </style>
</head>
<body>
    <h1>Real-Time Chat Application</h1>

    <div id="userInfo">
        <input type="text" id="usernameInput" placeholder="Enter your username" />
        <button id="joinButton">Join Chat</button>
        <span id="userCount">Users online: 0</span>
    </div>

    <div id="chatContainer" style="display: none;">
        <div id="messages"></div>
        <div id="typingIndicator" class="typing-indicator"></div>
        <div>
            <input type="text" id="messageInput" placeholder="Type your message..." />
            <button id="sendButton">Send</button>
        </div>
    </div>

    <script src="/socket.io/socket.io.js"></script>
    <script>
        const socket = io();
        let username = '';
        let typingTimer;

        // DOM elements
        const usernameInput = document.getElementById('usernameInput');
        const joinButton = document.getElementById('joinButton');
        const chatContainer = document.getElementById('chatContainer');
        const userInfo = document.getElementById('userInfo');
        const messagesDiv = document.getElementById('messages');
        const messageInput = document.getElementById('messageInput');
        const sendButton = document.getElementById('sendButton');
        const userCount = document.getElementById('userCount');
        const typingIndicator = document.getElementById('typingIndicator');

        // Join chat functionality
        joinButton.addEventListener('click', () => {
            username = usernameInput.value.trim();
            if (username) {
                socket.emit('user-join', username);
                userInfo.style.display = 'none';
                chatContainer.style.display = 'block';
                messageInput.focus();
            }
        });

        // Send message functionality
        function sendMessage() {
            const message = messageInput.value.trim();
            if (message && username) {
                socket.emit('chat-message', { message: message });
                messageInput.value = '';
                socket.emit('typing', { isTyping: false });
            }
        }

        sendButton.addEventListener('click', sendMessage);
        messageInput.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') {
                sendMessage();
            }
        });

        // Typing indicator
        messageInput.addEventListener('input', () => {
            socket.emit('typing', { isTyping: true });
            clearTimeout(typingTimer);
            typingTimer = setTimeout(() => {
                socket.emit('typing', { isTyping: false });
            }, 1000);
        });

        // Socket event listeners
        socket.on('message', (data) => {
            const messageElement = document.createElement('div');
            messageElement.className = 'message';
            const timestamp = new Date(data.timestamp).toLocaleTimeString();
            messageElement.innerHTML = `
                <span class="username">${data.username}:</span> 
                ${data.message}
                <span class="timestamp">(${timestamp})</span>
            `;
            messagesDiv.appendChild(messageElement);
            messagesDiv.scrollTop = messagesDiv.scrollHeight;
        });

        socket.on('user-connected', (data) => {
            const messageElement = document.createElement('div');
            messageElement.innerHTML = `<em>${data.username} joined the chat</em>`;
            messageElement.style.color = '#28a745';
            messagesDiv.appendChild(messageElement);
            messagesDiv.scrollTop = messagesDiv.scrollHeight;
        });

        socket.on('user-disconnected', (data) => {
            const messageElement = document.createElement('div');
            messageElement.innerHTML = `<em>${data.username} left the chat</em>`;
            messageElement.style.color = '#dc3545';
            messagesDiv.appendChild(messageElement);
            messagesDiv.scrollTop = messagesDiv.scrollHeight;
        });

        socket.on('user-count', (count) => {
            userCount.textContent = `Users online: ${count}`;
        });

        socket.on('user-typing', (data) => {
            if (data.isTyping) {
                typingIndicator.textContent = `${data.username} is typing...`;
            } else {
                typingIndicator.textContent = '';
            }
        });

        // Handle connection status
        socket.on('connect', () => {
            console.log('Connected to server');
        });

        socket.on('disconnect', () => {
            console.log('Disconnected from server');
        });
    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Coding Examples

The above implementation demonstrates several key Socket.IO concepts:

Basic Server and Client Connection

The server listens for the connection event, which fires whenever a new client connects. Each connection receives a unique socket instance that represents that specific client:

io.on('connection', (socket) => {
  console.log(`User connected: ${socket.id}`);
  // Handle this specific client
});
Enter fullscreen mode Exit fullscreen mode

On the client side, establishing a connection is straightforward:

const socket = io();
Enter fullscreen mode Exit fullscreen mode

Emitting and Listening for Events

Socket.IO uses custom events for communication. The server can emit events to specific clients:

// Send to specific client
socket.emit('welcome', { message: 'Welcome to the chat!' });

// Listen for events from client
socket.on('chat-message', (data) => {
  console.log('Received message:', data.message);
});
Enter fullscreen mode Exit fullscreen mode

Clients can emit events to the server and listen for responses:

// Send to server
socket.emit('chat-message', { message: 'Hello, world!' });

// Listen for events from server
socket.on('message', (data) => {
  console.log('New message:', data);
});
Enter fullscreen mode Exit fullscreen mode

Broadcasting Messages

Broadcasting allows the server to send messages to multiple clients:

// Send to all connected clients
io.emit('announcement', { message: 'Server maintenance in 5 minutes' });

// Send to all clients except the sender
socket.broadcast.emit('user-connected', { username: 'John' });

// Send to specific room
io.to('room1').emit('room-message', { message: 'Room-specific message' });
Enter fullscreen mode Exit fullscreen mode

Handling Disconnections

Proper disconnection handling ensures clean resource management:

socket.on('disconnect', (reason) => {
  console.log(`User ${socket.id} disconnected: ${reason}`);

  // Clean up user data
  connectedUsers.delete(socket.id);

  // Notify other users
  socket.broadcast.emit('user-left', { userId: socket.id });

  // Update user count
  io.emit('user-count', connectedUsers.size);
});
Enter fullscreen mode Exit fullscreen mode

What's Next

Once you've mastered the basics, Socket.IO offers advanced features for complex applications:

Namespaces

Namespaces allow you to split your application logic across multiple communication channels:

// Server-side namespaces
const chatNamespace = io.of('/chat');
const gameNamespace = io.of('/game');

chatNamespace.on('connection', (socket) => {
  // Handle chat-specific connections
});

// Client-side namespace connection
const chatSocket = io('/chat');
const gameSocket = io('/game');
Enter fullscreen mode Exit fullscreen mode

Rooms

Rooms enable broadcasting to subsets of connected clients:

// Join a room
socket.join('room1');

// Leave a room
socket.leave('room1');

// Send to all clients in a room
io.to('room1').emit('room-update', data);

// Send to all clients in a room except sender
socket.to('room1').emit('room-message', data);
Enter fullscreen mode Exit fullscreen mode

Scaling with Redis Adapter

For production applications running multiple server instances, use the Redis adapter:

npm install @socket.io/redis-adapter redis

const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');

const pubClient = createClient({ host: 'localhost', port: 6379 });
const subClient = pubClient.duplicate();

io.adapter(createAdapter(pubClient, subClient));
Enter fullscreen mode Exit fullscreen mode

Security Considerations

Implement proper authentication and authorization:

// Authentication middleware
io.use((socket, next) => {
  const token = socket.handshake.auth.token;

  // Verify JWT token
  jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
    if (err) return next(new Error('Authentication error'));
    socket.userId = decoded.userId;
    next();
  });
});

// Rate limiting
const rateLimit = require('socket.io-rate-limit');
io.use(rateLimit({
  max: 100, // limit each IP to 100 requests per windowMs
  windowMs: 60000 // 1 minute
}));
Enter fullscreen mode Exit fullscreen mode

Conclusion

Socket.IO transforms how we build interactive web applications by providing seamless real-time communication between clients and servers. Its event-driven architecture, automatic reconnection capabilities, and rich feature set make it an excellent choice for applications requiring instant updates and bidirectional communication.

car

Top comments (0)