Socket.IO Implementation

Building Robust Real-time Applications with Socket.IO

Introduction to Socket.IO

In our previous lecture, we explored WebSockets and their fundamental role in real-time web applications. Today, we'll delve deeper into Socket.IO, a powerful library that builds upon WebSockets and provides additional features that make real-time applications easier to build and more reliable.

flowchart TD A[Socket.IO] --> B[WebSocket Support] A --> C[Fallback Transport Methods] A --> D[Advanced Features] B --> B1[Low-latency Bidirectional Communication] C --> C1[Long Polling] C --> C2[HTTP Polling] C --> C3[WebTransport] D --> D1[Auto-reconnection] D --> D2[Room-based Broadcasting] D --> D3[Namespaces] D --> D4[Acknowledgements]

Why Socket.IO over Raw WebSockets?

Banking Application Analogy

Think of raw WebSockets as a simple bank transaction: you send money directly from one account to another. It works when everything is functioning perfectly, but there are no built-in safeguards.

Socket.IO, on the other hand, is like a modern banking system with multiple features:

  • If direct transfers fail, it tries alternative methods automatically (fallback transports)
  • If your internet connection drops during a transaction, it retries when you're back online (reconnection)
  • You can schedule recurring payments (event handling)
  • You can make one transfer to multiple recipients (broadcasting)
  • Different accounts are organized by purpose - business, personal, savings (namespaces and rooms)
  • You get confirmation when transactions complete (acknowledgments)

Setting Up Socket.IO

Installing Socket.IO


// For the server-side
npm install socket.io

// For the client-side (browser)
// The client library is served by the server in development
// For production, you can install it separately:
npm install socket.io-client
                

Basic Server Setup


// server.js - Basic Socket.IO server with Express
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const path = require('path');

// Create Express app
const app = express();
const server = http.createServer(app);

// Create Socket.IO server by passing the HTTP server
const io = new Server(server);

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

// Socket.IO connection handling
io.on('connection', (socket) => {
  console.log('A user connected:', socket.id);
  
  // Listen for custom events from client
  socket.on('chat message', (msg) => {
    console.log('Message received:', msg);
    
    // Broadcast the message to all connected clients
    io.emit('chat message', msg);
  });
  
  // Disconnection event
  socket.on('disconnect', () => {
    console.log('User disconnected:', socket.id);
  });
});

// Start the server
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`);
});
                

Basic Client Implementation


// 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>Socket.IO Chat</title>
    <style>
        #messages { list-style-type: none; margin: 0; padding: 0; }
        #messages li { padding: 0.5rem 1rem; }
        #messages li:nth-child(odd) { background: #f0f0f0; }
        form { display: flex; padding: 1rem; }
        input { flex-grow: 1; margin-right: 0.5rem; padding: 0.5rem; }
        button { padding: 0.5rem 1rem; }
    </style>
</head>
<body>
    <ul id="messages"></ul>
    <form id="form">
        <input id="input" autocomplete="off" />
        <button>Send</button>
    </form>
    
    <script src="/socket.io/socket.io.js"></script>
    <script>
        // Connect to Socket.IO server
        const socket = io();
        
        // DOM elements
        const form = document.getElementById('form');
        const input = document.getElementById('input');
        const messages = document.getElementById('messages');
        
        // Form submission handler
        form.addEventListener('submit', (e) => {
            e.preventDefault();
            
            if (input.value) {
                // Emit chat message event to server
                socket.emit('chat message', input.value);
                input.value = '';
            }
        });
        
        // Listen for chat messages from server
        socket.on('chat message', (msg) => {
            const item = document.createElement('li');
            item.textContent = msg;
            messages.appendChild(item);
            window.scrollTo(0, document.body.scrollHeight);
        });
        
        // Connection events
        socket.on('connect', () => {
            const item = document.createElement('li');
            item.textContent = 'Connected to server';
            item.style.color = 'green';
            messages.appendChild(item);
        });
        
        socket.on('disconnect', () => {
            const item = document.createElement('li');
            item.textContent = 'Disconnected from server';
            item.style.color = 'red';
            messages.appendChild(item);
        });
    </script>
</body>
</html>
                

Advanced Connection Options


// Server with custom configuration
const io = new Server(server, {
  // Specify the path (default is /socket.io)
  path: '/real-time',
  
  // CORS configuration
  cors: {
    origin: ["https://example.com", "https://admin.example.com"],
    methods: ["GET", "POST"],
    credentials: true
  },
  
  // Socket.IO server options
  connectTimeout: 45000, // Default is 45 seconds
  maxHttpBufferSize: 1e6, // 1MB (default is 1MB)
  pingTimeout: 20000, // How many ms without a pong packet to consider the connection closed
  pingInterval: 25000, // How many ms before sending a new ping packet
  upgradeTimeout: 10000, // Default is 10 seconds
  allowUpgrades: true, // Default is true (allows transport upgrades)
  transports: ['polling', 'websocket'], // Available transports (polling is always enabled)
  allowEIO3: false // Default is false (enforce the latest EIO version)
});

// Client with custom configuration
const socket = io('https://server.example.com', {
  path: '/real-time',
  
  // Transport options
  transports: ['websocket', 'polling'], // Try WebSocket first, fallback to polling
  upgrade: true, // Default is true (allow transport upgrades)
  
  // Reconnection options
  reconnection: true, // Default is true (enable automatic reconnection)
  reconnectionAttempts: Infinity, // Default is Infinity (unlimited attempts)
  reconnectionDelay: 1000, // Default is 1000ms (first delay)
  reconnectionDelayMax: 5000, // Default is 5000ms (maximum delay)
  randomizationFactor: 0.5, // Default is 0.5 (multiplier for random jitter)
  
  // Connection timeout
  timeout: 20000, // Default is 20000ms (connection timeout)
  
  // Authentication
  auth: {
    token: "user-jwt-token"
  },
  
  // Headers for HTTP polling transport
  extraHeaders: {
    "X-Custom-Header": "custom header value"
  },
});
                

Core Socket.IO Concepts

Event-Based Communication

Socket.IO uses an event-driven architecture where clients and servers communicate by emitting and listening for named events, similar to the Observer pattern in software design.


// Server-side events
io.on('connection', (socket) => {
  // Listen for custom events
  socket.on('user action', (data) => {
    console.log('User action:', data);
  });
  
  // Emit events to client
  socket.emit('server update', { status: 'processing', progress: 50 });
  
  // Listen for event with acknowledgment
  socket.on('request data', (query, callback) => {
    // Process request
    const result = processQuery(query);
    
    // Send back acknowledgment with data
    callback({
      success: true,
      data: result
    });
  });
});

// Client-side events
const socket = io();

// Listen for events from server
socket.on('server update', (data) => {
  console.log('Server update:', data);
  updateProgressBar(data.progress);
});

// Emit events to server
socket.emit('user action', { type: 'click', element: 'signup-button' });

// Emit with acknowledgment
socket.emit('request data', { user: 123 }, (response) => {
  if (response.success) {
    displayData(response.data);
  } else {
    showError(response.error);
  }
});
                

This event-based system provides several advantages:

Broadcasting

Socket.IO makes it easy to send messages to multiple clients using broadcasting functions:


// Server-side broadcasting
io.on('connection', (socket) => {
  // Broadcast to all connected clients
  io.emit('global message', { 
    text: 'System broadcast message', 
    time: new Date().toISOString() 
  });
  
  // Broadcast to all clients except the sender
  socket.broadcast.emit('user joined', {
    username: socket.data.username,
    id: socket.id,
    time: new Date().toISOString()
  });
  
  socket.on('chat message', (msg) => {
    // Emit to specific room
    io.to('room1').emit('chat message', {
      user: socket.data.username,
      text: msg,
      time: new Date().toISOString()
    });
    
    // Emit to multiple rooms
    io.to('room1').to('room2').emit('announcement', 'This is an important notice');
    
    // Broadcast to a room (all in room except sender)
    socket.to('room1').emit('typing', {
      username: socket.data.username,
      isTyping: true
    });
  });
});
                

Rooms and Namespaces

Socket.IO provides two levels of separation for organizing connections:

flowchart TD A[Socket.IO Server] --> B[Default Namespace /] A --> C[Custom Namespace /admin] A --> D[Custom Namespace /chat] B --> B1[Room: general] B --> B2[Room: support] C --> C1[Room: dashboard] C --> C2[Room: monitoring] D --> D1[Room: javascript] D --> D2[Room: python] D --> D3[Room: private-alice-bob] E1[Client 1] --> B1 E2[Client 2] --> B1 E3[Client 3] --> B2 F1[Admin 1] --> C1 F1 --> C2 G1[User 1] --> D1 G2[User 2] --> D1 G2 --> D3 G3[User 3] --> D2 G3 --> D3

// Server-side namespaces and rooms
const io = require('socket.io')(server);

// Default namespace (/)
io.on('connection', (socket) => {
  console.log('Connection to default namespace:', socket.id);
  
  // Join a room
  socket.on('join room', (room) => {
    socket.join(room);
    socket.emit('room joined', room);
    socket.to(room).emit('user joined room', {
      id: socket.id,
      room: room
    });
  });
  
  // Leave a room
  socket.on('leave room', (room) => {
    socket.leave(room);
    socket.emit('room left', room);
    socket.to(room).emit('user left room', {
      id: socket.id,
      room: room
    });
  });
});

// Admin namespace (/admin)
const adminNamespace = io.of('/admin');

adminNamespace.on('connection', (socket) => {
  console.log('Admin connected:', socket.id);
  
  // Join admin to dashboard room automatically
  socket.join('dashboard');
  
  // Admin-specific events
  socket.on('system announcement', (message) => {
    // Broadcast to all clients in default namespace
    io.emit('announcement', {
      type: 'system',
      message: message,
      from: 'Admin'
    });
  });
});

// Chat namespace (/chat)
const chatNamespace = io.of('/chat');

chatNamespace.on('connection', (socket) => {
  console.log('User connected to chat:', socket.id);
  
  // Set user data
  socket.data.username = socket.handshake.auth.username;
  
  // Join public room
  socket.join('public');
  
  // Handle private messaging
  socket.on('private message', (data) => {
    // Create or join a private room between the two users
    const room = [socket.id, data.to].sort().join('-');
    socket.join(room);
    
    // Notify the target user to join the room too
    socket.to(data.to).emit('private room invite', {
      from: socket.id,
      room: room
    });
    
    // Send the message to the room
    chatNamespace.to(room).emit('private message', {
      content: data.content,
      from: socket.id,
      to: data.to
    });
  });
});
                

// Client-side namespaces and rooms
// Connect to default namespace
const mainSocket = io();

// Connect to admin namespace with authentication
const adminSocket = io('/admin', {
  auth: {
    token: 'admin-secret-token'
  }
});

// Connect to chat namespace
const chatSocket = io('/chat', {
  auth: {
    username: 'Alice'
  }
});

// Join a room in the main namespace
function joinRoom(room) {
  mainSocket.emit('join room', room);
}

// Listen for room events
mainSocket.on('room joined', (room) => {
  console.log(`Joined room: ${room}`);
  // Update UI to show current room
});

mainSocket.on('user joined room', (data) => {
  console.log(`User ${data.id} joined room ${data.room}`);
  // Show notification
});

// Admin namespace functionality
adminSocket.on('connect', () => {
  console.log('Connected to admin namespace');
  
  // Admin UI elements become available
  document.getElementById('admin-panel').style.display = 'block';
});

adminSocket.on('connect_error', (err) => {
  console.error('Admin connection error:', err.message);
  
  // Show authentication error
  document.getElementById('admin-error').textContent = 'Admin authentication failed';
});

// Send announcement as admin
function sendAnnouncement(message) {
  adminSocket.emit('system announcement', message);
}

// Chat namespace functionality
chatSocket.on('connect', () => {
  console.log('Connected to chat namespace');
});

// Handle private messaging
chatSocket.on('private room invite', (data) => {
  chatSocket.join(data.room);
  console.log(`Joined private room with ${data.from}`);
});

chatSocket.on('private message', (data) => {
  console.log(`Private message in room from ${data.from} to ${data.to}: ${data.content}`);
  // Update UI with private message
});

// Send private message
function sendPrivateMessage(to, content) {
  chatSocket.emit('private message', { to, content });
}
                

Middlewares

Socket.IO provides middleware hooks for connection and packet handling:


// Connection middleware (executed before the connection event)
io.use((socket, next) => {
  // Get authentication token
  const token = socket.handshake.auth.token;
  
  // Validate token (example)
  validateToken(token)
    .then(user => {
      // Store user data on the socket for later use
      socket.data.user = user;
      next(); // Continue with the connection
    })
    .catch(err => {
      // Reject the connection with an error
      next(new Error('Authentication error: ' + err.message));
    });
});

// Namespace-specific middleware
const adminNamespace = io.of('/admin');

adminNamespace.use((socket, next) => {
  // Check if user has admin role
  if (socket.data.user && socket.data.user.role === 'admin') {
    next();
  } else {
    next(new Error('Access denied: Admin privileges required'));
  }
});

// Socket middleware (executed for each incoming packet)
io.on('connection', (socket) => {
  // Add packet middleware to this socket
  socket.use((packet, next) => {
    const [event, data] = packet;
    
    // Log all events
    console.log(`Event: ${event}, Data:`, data);
    
    // Validate event data
    if (event === 'chat message') {
      if (typeof data !== 'string' || data.trim() === '') {
        return next(new Error('Invalid message format'));
      }
      
      // Filter inappropriate content
      if (containsProfanity(data)) {
        return next(new Error('Message contains inappropriate content'));
      }
    }
    
    // Continue processing the packet
    next();
  });
  
  // Handle middleware errors
  socket.on('error', (err) => {
    console.error('Socket middleware error:', err.message);
    
    // Notify client about the error
    socket.emit('error', {
      message: err.message
    });
  });
});

// Helper function to validate token (example)
function validateToken(token) {
  return new Promise((resolve, reject) => {
    // In a real application, verify against a database or JWT
    if (token === 'valid-token') {
      resolve({ id: 1, username: 'john', role: 'user' });
    } else if (token === 'admin-token') {
      resolve({ id: 2, username: 'admin', role: 'admin' });
    } else {
      reject(new Error('Invalid token'));
    }
  });
}

// Helper function to check for inappropriate content
function containsProfanity(message) {
  const profanityList = ['bad1', 'bad2', 'bad3'];
  return profanityList.some(word => message.toLowerCase().includes(word));
}
                

Advanced Socket.IO Features

Handling Volatile Events

For real-time data where missing updates is acceptable (like cursor positions or typing indicators), Socket.IO provides volatile events that won't be buffered during disconnections:


// Server-side volatile events
io.on('connection', (socket) => {
  // Regular event (will be buffered during disconnection)
  socket.emit('important update', { data: 'This message will be queued if client disconnects' });
  
  // Volatile event (will be dropped during disconnection)
  socket.volatile.emit('cursor position', { x: 125, y: 350 });
  
  // Room-specific volatile event
  socket.to('game-room').volatile.emit('player movement', {
    playerId: socket.id,
    position: { x: 125, y: 350 },
    timestamp: Date.now()
  });
});

// Client-side volatile events
const socket = io();

// Send regular event
socket.emit('chat message', 'Hello, this is important!');

// Send volatile event
socket.volatile.emit('typing', true);

// Listen for volatile events
socket.on('cursor position', (data) => {
  updateCursorPosition(data.x, data.y);
});
                

Binary Data

Socket.IO supports sending binary data like files, images, or any ArrayBuffer/Blob:


// Server-side binary data handling
io.on('connection', (socket) => {
  socket.on('file upload', (data, callback) => {
    console.log('Received file, size:', data.byteLength);
    
    // Process the file (example: save to disk)
    const filename = `upload-${Date.now()}.bin`;
    const buffer = Buffer.from(data);
    
    require('fs').writeFile(filename, buffer, (err) => {
      if (err) {
        console.error('File save error:', err);
        callback({ success: false, error: err.message });
      } else {
        console.log('File saved:', filename);
        callback({ success: true, filename });
      }
    });
  });
  
  // Send binary data to client
  socket.on('request image', (id) => {
    // Example: Read an image from disk
    require('fs').readFile(`./images/${id}.jpg`, (err, data) => {
      if (err) {
        socket.emit('image error', { id, error: err.message });
      } else {
        // Send the binary data
        socket.emit('image data', { id, data });
      }
    });
  });
});

// Client-side binary data handling
const socket = io();

// File input change handler
document.getElementById('file-input').addEventListener('change', (e) => {
  const file = e.target.files[0];
  if (!file) return;
  
  const reader = new FileReader();
  
  reader.onload = (event) => {
    // Get the ArrayBuffer
    const arrayBuffer = event.target.result;
    
    // Upload the file
    socket.emit('file upload', arrayBuffer, (response) => {
      if (response.success) {
        showSuccess(`File uploaded successfully as ${response.filename}`);
      } else {
        showError(`File upload failed: ${response.error}`);
      }
    });
  };
  
  reader.readAsArrayBuffer(file);
});

// Request an image
function requestImage(id) {
  socket.emit('request image', id);
}

// Handle incoming image data
socket.on('image data', (data) => {
  // Convert ArrayBuffer to Blob
  const blob = new Blob([data.data]);
  
  // Create an object URL
  const url = URL.createObjectURL(blob);
  
  // Display the image
  const img = document.getElementById(`image-${data.id}`);
  img.src = url;
  
  // Clean up the object URL when done
  img.onload = () => {
    URL.revokeObjectURL(url);
  };
});

socket.on('image error', (data) => {
  console.error(`Error loading image ${data.id}:`, data.error);
  // Show error in UI
});
                

Error Handling

Robust error handling is essential for production applications:


// Server-side error handling
io.on('connection', (socket) => {
  // Handle middleware errors
  socket.on('error', (err) => {
    console.error('Socket error:', err.message);
    socket.emit('server error', { message: 'Internal socket error' });
  });
  
  // Handle event errors
  socket.on('process data', (data) => {
    try {
      const result = processData(data);
      socket.emit('process result', { success: true, result });
    } catch (err) {
      console.error('Data processing error:', err);
      socket.emit('process result', { 
        success: false, 
        error: err.message 
      });
    }
  });
  
  // Handle async event errors
  socket.on('async operation', async (data) => {
    try {
      const result = await someAsyncOperation(data);
      socket.emit('async result', { success: true, result });
    } catch (err) {
      console.error('Async operation error:', err);
      socket.emit('async result', { 
        success: false, 
        error: err.message 
      });
    }
  });
});

// Client-side error handling
const socket = io({
  // Reconnection options
  reconnection: true,
  reconnectionAttempts: 5,
  reconnectionDelay: 1000,
  reconnectionDelayMax: 5000,
  randomizationFactor: 0.5
});

// Connection error handling
socket.on('connect_error', (err) => {
  console.error('Connection error:', err.message);
  showConnectionError('Unable to connect to the server');
});

socket.on('connect_timeout', () => {
  console.error('Connection timeout');
  showConnectionError('Connection timed out');
});

socket.on('reconnect_attempt', (attemptNumber) => {
  console.log(`Attempting to reconnect: ${attemptNumber}/5`);
  showReconnectingStatus(attemptNumber);
});

socket.on('reconnect', (attemptNumber) => {
  console.log(`Reconnected after ${attemptNumber} attempts`);
  hideConnectionError();
  showNotification('Reconnected to server');
});

socket.on('reconnect_error', (err) => {
  console.error('Reconnection error:', err.message);
});

socket.on('reconnect_failed', () => {
  console.error('Failed to reconnect after 5 attempts');
  showConnectionError('Could not reconnect to the server - please refresh the page');
});

// Server error events
socket.on('server error', (data) => {
  console.error('Server error:', data.message);
  showError(`Server error: ${data.message}`);
});

// Operation-specific error handling
socket.emit('process data', { /* ... */ }, (response) => {
  if (response.success) {
    showResult(response.result);
  } else {
    showError(`Operation failed: ${response.error}`);
  }
});

// UI helper functions (example)
function showConnectionError(message) {
  const errorElement = document.getElementById('connection-error');
  errorElement.textContent = message;
  errorElement.style.display = 'block';
}

function hideConnectionError() {
  document.getElementById('connection-error').style.display = 'none';
}

function showReconnectingStatus(attempt) {
  const statusElement = document.getElementById('connection-status');
  statusElement.textContent = `Reconnecting: Attempt ${attempt}/5`;
  statusElement.style.display = 'block';
}

function showNotification(message) {
  // Show a notification to the user
}

function showError(message) {
  // Display error message in UI
}

function showResult(result) {
  // Display operation result in UI
}
                

Redis Adapter for Scaling

For scaling Socket.IO across multiple servers, the Redis adapter enables message sharing:


// server.js
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');

const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer);

// Create Redis clients
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();

// Set up Redis adapter
async function setup() {
  // Connect Redis clients
  await Promise.all([pubClient.connect(), subClient.connect()]);
  
  // Create and use Redis adapter
  io.adapter(createAdapter(pubClient, subClient));
  
  console.log('Redis adapter initialized');
  
  // Standard Socket.IO connection handling
  io.on('connection', (socket) => {
    console.log('Client connected:', socket.id);
    
    // Join a room
    socket.on('join room', (room) => {
      socket.join(room);
      io.to(room).emit('user joined', {
        id: socket.id,
        room: room
      });
    });
    
    // Send message to room
    socket.on('room message', (data) => {
      io.to(data.room).emit('room message', {
        id: socket.id,
        room: data.room,
        message: data.message
      });
    });
  });
  
  // Start server
  const PORT = process.env.PORT || 3000;
  httpServer.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
  });
}

// Handle setup errors
setup().catch(err => {
  console.error('Error setting up Socket.IO with Redis:', err);
  process.exit(1);
});

// Graceful shutdown
process.on('SIGINT', async () => {
  await Promise.all([
    pubClient.quit(),
    subClient.quit()
  ]);
  process.exit(0);
});
                

Real-world Socket.IO Applications

Chat Application

Let's implement a more complete chat application with Socket.IO:


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

const app = express();
const server = http.createServer(app);
const io = new Server(server);

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

// Store users and chat rooms
const users = {};
const chatRooms = ['general', 'technology', 'random'];

// Socket.IO connection handling
io.on('connection', (socket) => {
  console.log('New user connected:', socket.id);
  
  // User login
  socket.on('user login', (username, callback) => {
    // Check if username is already taken
    if (getUserByName(username)) {
      return callback({ success: false, error: 'Username already taken' });
    }
    
    // Store user information
    users[socket.id] = {
      id: socket.id,
      username: username,
      rooms: ['general'] // Join general room by default
    };
    
    // Join the general room
    socket.join('general');
    
    // Notify user of successful login
    callback({ 
      success: true, 
      user: users[socket.id],
      rooms: chatRooms
    });
    
    // Notify others of new user
    socket.to('general').emit('user joined', {
      id: socket.id,
      username: username,
      room: 'general'
    });
    
    // Send current online users in general room
    const roomUsers = getOnlineUsersInRoom('general');
    socket.emit('room users', {
      room: 'general',
      users: roomUsers
    });
  });
  
  // Join room
  socket.on('join room', (room, callback) => {
    // Check if user is logged in
    if (!users[socket.id]) {
      return callback({ success: false, error: 'Not logged in' });
    }
    
    // Check if room exists
    if (!chatRooms.includes(room)) {
      return callback({ success: false, error: 'Room does not exist' });
    }
    
    // Join room
    socket.join(room);
    users[socket.id].rooms.push(room);
    
    // Notify user of successful join
    callback({ success: true, room });
    
    // Notify room of new user
    socket.to(room).emit('user joined', {
      id: socket.id,
      username: users[socket.id].username,
      room: room
    });
    
    // Send current online users in room
    const roomUsers = getOnlineUsersInRoom(room);
    io.to(room).emit('room users', {
      room: room,
      users: roomUsers
    });
  });
  
  // Leave room
  socket.on('leave room', (room, callback) => {
    // Check if user is logged in
    if (!users[socket.id]) {
      return callback({ success: false, error: 'Not logged in' });
    }
    
    // Cannot leave general room
    if (room === 'general') {
      return callback({ success: false, error: 'Cannot leave general room' });
    }
    
    // Leave room
    socket.leave(room);
    users[socket.id].rooms = users[socket.id].rooms.filter(r => r !== room);
    
    // Notify user of successful leave
    callback({ success: true, room });
    
    // Notify room of user leaving
    socket.to(room).emit('user left', {
      id: socket.id,
      username: users[socket.id].username,
      room: room
    });
    
    // Update online users in room
    const roomUsers = getOnlineUsersInRoom(room);
    io.to(room).emit('room users', {
      room: room,
      users: roomUsers
    });
  });
  
  // Chat message
  socket.on('chat message', (data, callback) => {
    // Check if user is logged in
    if (!users[socket.id]) {
      return callback({ success: false, error: 'Not logged in' });
    }
    
    // Check if message is valid
    if (!data.message || !data.room) {
      return callback({ success: false, error: 'Invalid message' });
    }
    
    // Check if user is in the room
    if (!users[socket.id].rooms.includes(data.room)) {
      return callback({ success: false, error: 'Not in room' });
    }
    
    // Create message object
    const message = {
      id: Date.now(),
      user: {
        id: socket.id,
        username: users[socket.id].username
      },
      room: data.room,
      text: data.message,
      timestamp: new Date().toISOString()
    };
    
    // Send message to room
    io.to(data.room).emit('chat message', message);
    
    // Acknowledge successful send
    callback({ success: true, message });
  });
  
  // Typing indicator
  socket.on('typing', (data) => {
    // Check if user is logged in
    if (!users[socket.id]) return;
    
    // Check if room is valid
    if (!data.room || !users[socket.id].rooms.includes(data.room)) return;
    
    // Broadcast typing status to room
    socket.to(data.room).emit('typing', {
      user: {
        id: socket.id,
        username: users[socket.id].username
      },
      room: data.room,
      isTyping: data.isTyping
    });
  });
  
  // Private messaging
  socket.on('private message', (data, callback) => {
    // Check if user is logged in
    if (!users[socket.id]) {
      return callback({ success: false, error: 'Not logged in' });
    }
    
    // Check if target user exists
    if (!data.to || !users[data.to]) {
      return callback({ success: false, error: 'User not found' });
    }
    
    // Create private message object
    const message = {
      id: Date.now(),
      from: {
        id: socket.id,
        username: users[socket.id].username
      },
      to: {
        id: data.to,
        username: users[data.to].username
      },
      text: data.message,
      timestamp: new Date().toISOString()
    };
    
    // Send message to recipient and sender
    io.to(data.to).to(socket.id).emit('private message', message);
    
    // Acknowledge successful send
    callback({ success: true, message });
  });
  
  // Disconnect
  socket.on('disconnect', () => {
    // Check if user was logged in
    if (!users[socket.id]) return;
    
    const username = users[socket.id].username;
    const userRooms = [...users[socket.id].rooms];
    
    // Notify all user's rooms of disconnection
    userRooms.forEach(room => {
      socket.to(room).emit('user left', {
        id: socket.id,
        username: username,
        room: room
      });
      
      // Update online users in room
      const roomUsers = getOnlineUsersInRoom(room, socket.id);
      io.to(room).emit('room users', {
        room: room,
        users: roomUsers
      });
    });
    
    // Remove user from users object
    delete users[socket.id];
    console.log('User disconnected:', username);
  });
});

// Helper: Get user by username
function getUserByName(username) {
  return Object.values(users).find(user => user.username === username);
}

// Helper: Get online users in a room
function getOnlineUsersInRoom(room, excludeId = null) {
  return Object.values(users)
    .filter(user => user.rooms.includes(room) && user.id !== excludeId)
    .map(user => ({
      id: user.id,
      username: user.username
    }));
}

// Start the server
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`);
});
                

// public/app.js
// Chat application client-side code
document.addEventListener('DOMContentLoaded', () => {
  // DOM elements
  const loginForm = document.getElementById('login-form');
  const chatInterface = document.getElementById('chat-interface');
  const messageForm = document.getElementById('message-form');
  const messageInput = document.getElementById('message-input');
  const messagesContainer = document.getElementById('messages');
  const roomsList = document.getElementById('rooms-list');
  const usersList = document.getElementById('users-list');
  const roomTitle = document.getElementById('room-title');
  const usersTitle = document.getElementById('users-title');
  const typingIndicator = document.getElementById('typing-indicator');
  
  // Application state
  const state = {
    user: null,
    currentRoom: 'general',
    typingTimeout: null,
    rooms: []
  };
  
  // Connect to Socket.IO server
  const socket = io();
  
  // Connection events
  socket.on('connect', () => {
    console.log('Connected to server');
    
    // If user is logged in, attempt to re-login on reconnection
    if (state.user) {
      socket.emit('user login', state.user.username, (response) => {
        if (response.success) {
          state.user = response.user;
          
          // Rejoin rooms
          state.rooms.forEach(room => {
            if (room !== 'general') { // Already joined general by default
              socket.emit('join room', room, () => {});
            }
          });
        }
      });
    }
  });
  
  socket.on('disconnect', () => {
    console.log('Disconnected from server');
    addSystemMessage('Disconnected from server');
  });
  
  socket.on('connect_error', () => {
    console.log('Connection error');
    addSystemMessage('Connection error - attempting to reconnect');
  });
  
  socket.on('reconnect', (attempt) => {
    console.log(`Reconnected after ${attempt} attempts`);
    addSystemMessage('Reconnected to server');
  });
  
  // Login form submission
  loginForm.addEventListener('submit', (e) => {
    e.preventDefault();
    
    const username = document.getElementById('username-input').value.trim();
    
    if (!username) {
      showError('Please enter a username');
      return;
    }
    
    socket.emit('user login', username, (response) => {
      if (response.success) {
        state.user = response.user;
        state.rooms = response.rooms;
        
        // Show chat interface
        loginForm.style.display = 'none';
        chatInterface.style.display = 'flex';
        
        // Initialize UI
        roomTitle.textContent = 'Room: general';
        populateRoomsList(response.rooms);
        
        // Add welcome message
        addSystemMessage(`Welcome to the chat, ${username}!`);
      } else {
        showError(response.error);
      }
    });
  });
  
  // Message form submission
  messageForm.addEventListener('submit', (e) => {
    e.preventDefault();
    
    const message = messageInput.value.trim();
    
    if (!message) return;
    
    // Send message to current room
    socket.emit('chat message', {
      room: state.currentRoom,
      message: message
    }, (response) => {
      if (!response.success) {
        showError(response.error);
      }
    });
    
    // Clear input
    messageInput.value = '';
    
    // Clear typing indicator
    clearTimeout(state.typingTimeout);
    socket.emit('typing', {
      room: state.currentRoom,
      isTyping: false
    });
  });
  
  // Typing indicator
  messageInput.addEventListener('input', () => {
    // Don't emit typing events if not logged in
    if (!state.user) return;
    
    // Clear existing timeout
    clearTimeout(state.typingTimeout);
    
    // Emit typing event
    socket.emit('typing', {
      room: state.currentRoom,
      isTyping: true
    });
    
    // Set timeout to clear typing indicator
    state.typingTimeout = setTimeout(() => {
      socket.emit('typing', {
        room: state.currentRoom,
        isTyping: false
      });
    }, 2000);
  });
  
  // Room click handler
  roomsList.addEventListener('click', (e) => {
    if (e.target.classList.contains('room-item')) {
      const room = e.target.dataset.room;
      
      // Don't do anything if already in this room
      if (room === state.currentRoom) return;
      
      // Join room if not already joined
      if (!state.user.rooms.includes(room)) {
        socket.emit('join room', room, (response) => {
          if (response.success) {
            switchRoom(room);
          } else {
            showError(response.error);
          }
        });
      } else {
        // Already joined, just switch UI
        switchRoom(room);
      }
    }
  });
  
  // Handle chat messages
  socket.on('chat message', (message) => {
    // Only show messages for current room
    if (message.room === state.currentRoom) {
      addChatMessage(message);
    }
  });
  
  // Handle private messages
  socket.on('private message', (message) => {
    addPrivateMessage(message);
  });
  
  // Handle user join events
  socket.on('user joined', (data) => {
    // Only show for current room
    if (data.room === state.currentRoom) {
      addSystemMessage(`${data.username} has joined the room`);
    }
  });
  
  // Handle user leave events
  socket.on('user left', (data) => {
    // Only show for current room
    if (data.room === state.currentRoom) {
      addSystemMessage(`${data.username} has left the room`);
    }
  });
  
  // Handle room users update
  socket.on('room users', (data) => {
    // Only update for current room
    if (data.room === state.currentRoom) {
      updateUsersList(data.users);
    }
  });
  
  // Handle typing indicators
  socket.on('typing', (data) => {
    // Only show for current room
    if (data.room === state.currentRoom) {
      if (data.isTyping) {
        typingIndicator.textContent = `${data.user.username} is typing...`;
      } else {
        typingIndicator.textContent = '';
      }
    }
  });
  
  // Helper: Switch to a different room
  function switchRoom(room) {
    // Update state
    state.currentRoom = room;
    
    // Update UI
    roomTitle.textContent = `Room: ${room}`;
    
    // Clear messages
    messagesContainer.innerHTML = '';
    
    // Add room switch message
    addSystemMessage(`Switched to room: ${room}`);
    
    // Update rooms list
    Array.from(roomsList.children).forEach(item => {
      if (item.dataset.room === room) {
        item.classList.add('active');
      } else {
        item.classList.remove('active');
      }
    });
    
    // Clear typing indicator
    typingIndicator.textContent = '';
  }
  
  // Helper: Populate rooms list
  function populateRoomsList(rooms) {
    roomsList.innerHTML = '';
    
    rooms.forEach(room => {
      const li = document.createElement('li');
      li.textContent = room;
      li.className = 'room-item';
      li.dataset.room = room;
      
      if (room === state.currentRoom) {
        li.classList.add('active');
      }
      
      roomsList.appendChild(li);
    });
  }
  
  // Helper: Update users list
  function updateUsersList(users) {
    usersList.innerHTML = '';
    usersTitle.textContent = `Users (${users.length})`;
    
    users.forEach(user => {
      const li = document.createElement('li');
      li.textContent = user.username;
      li.className = 'user-item';
      li.dataset.id = user.id;
      
      usersList.appendChild(li);
    });
  }
  
  // Helper: Add chat message to UI
  function addChatMessage(message) {
    const messageElement = document.createElement('div');
    messageElement.className = 'message';
    
    // Add special class if message is from current user
    if (message.user.id === socket.id) {
      messageElement.classList.add('own-message');
    }
    
    const timeString = new Date(message.timestamp).toLocaleTimeString();
    
    messageElement.innerHTML = `
      ${message.user.username}
      ${escapeHTML(message.text)}
      ${timeString}
    `;
    
    messagesContainer.appendChild(messageElement);
    messagesContainer.scrollTop = messagesContainer.scrollHeight;
  }
  
  // Helper: Add private message to UI
  function addPrivateMessage(message) {
    const messageElement = document.createElement('div');
    messageElement.className = 'message private-message';
    
    // Add special class if message is from current user
    if (message.from.id === socket.id) {
      messageElement.classList.add('own-message');
    }
    
    const timeString = new Date(message.timestamp).toLocaleTimeString();
    
    messageElement.innerHTML = `
      PRIVATE
      ${message.from.id === socket.id ? 'You to ' + message.to.username : message.from.username}
      ${escapeHTML(message.text)}
      ${timeString}
    `;
    
    messagesContainer.appendChild(messageElement);
    messagesContainer.scrollTop = messagesContainer.scrollHeight;
  }
  
  // Helper: Add system message to UI
  function addSystemMessage(text) {
    const messageElement = document.createElement('div');
    messageElement.className = 'message system-message';
    messageElement.textContent = text;
    
    messagesContainer.appendChild(messageElement);
    messagesContainer.scrollTop = messagesContainer.scrollHeight;
  }
  
  // Helper: Show error message
  function showError(message) {
    const errorElement = document.getElementById('error-message');
    errorElement.textContent = message;
    errorElement.style.display = 'block';
    
    setTimeout(() => {
      errorElement.style.display = 'none';
    }, 5000);
  }
  
  // Helper: Escape HTML for security
  function escapeHTML(text) {
    return text
      .replace(/&/g, '&')
      .replace(//g, '>')
      .replace(/"/g, '"')
      .replace(/'/g, ''');
  }
});
                

<!-- 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>Socket.IO Chat Application</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background-color: #f5f5f5;
            height: 100vh;
            padding: 20px;
        }
        
        .container {
            max-width: 1000px;
            margin: 0 auto;
            background: white;
            height: 100%;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            border-radius: 5px;
        }
        
        .login-form {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            height: 100%;
            padding: 20px;
        }
        
        .login-form h2 {
            margin-bottom: 20px;
        }
        
        .login-form input {
            padding: 10px;
            width: 300px;
            margin-bottom: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        
        .login-form button {
            padding: 10px 20px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        
        .chat-interface {
            display: none;
            height: 100%;
        }
        
        .sidebar {
            width: 250px;
            background-color: #f0f0f0;
            padding: 15px;
            border-right: 1px solid #ddd;
            display: flex;
            flex-direction: column;
        }
        
        .rooms-container, .users-container {
            margin-bottom: 20px;
        }
        
        .sidebar h3 {
            margin-bottom: 10px;
            border-bottom: 1px solid #ddd;
            padding-bottom: 5px;
        }
        
        .room-item, .user-item {
            padding: 8px;
            margin-bottom: 5px;
            border-radius: 4px;
            cursor: pointer;
            list-style-type: none;
        }
        
        .room-item:hover {
            background-color: #e0e0e0;
        }
        
        .room-item.active {
            background-color: #ddd;
            font-weight: bold;
        }
        
        .chat-container {
            flex-grow: 1;
            display: flex;
            flex-direction: column;
            padding: 15px;
        }
        
        .chat-header {
            padding-bottom: 10px;
            border-bottom: 1px solid #ddd;
            margin-bottom: 10px;
        }
        
        .messages {
            flex-grow: 1;
            overflow-y: auto;
            padding: 10px;
            display: flex;
            flex-direction: column;
            gap: 10px;
        }
        
        .message {
            padding: 10px;
            border-radius: 5px;
            background-color: #f0f0f0;
            max-width: 80%;
            align-self: flex-start;
        }
        
        .message-user {
            font-weight: bold;
            margin-right: 5px;
        }
        
        .message-time {
            font-size: 0.8em;
            color: #666;
            margin-left: 5px;
        }
        
        .own-message {
            background-color: #dcf8c6;
            align-self: flex-end;
        }
        
        .system-message {
            background-color: #f8f8f8;
            color: #666;
            font-style: italic;
            max-width: 100%;
            align-self: center;
        }
        
        .private-message {
            background-color: #f0e0ff;
        }
        
        .message-private {
            font-size: 0.8em;
            background-color: #a64dff;
            color: white;
            padding: 2px 4px;
            border-radius: 3px;
            margin-right: 5px;
        }
        
        .typing-indicator {
            height: 20px;
            padding: 5px;
            color: #666;
            font-style: italic;
        }
        
        .message-form {
            display: flex;
            padding: 10px 0;
            border-top: 1px solid #ddd;
        }
        
        .message-form input {
            flex-grow: 1;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            margin-right: 10px;
        }
        
        .message-form button {
            padding: 10px 20px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        
        .error-message {
            background-color: #ffebee;
            color: #c62828;
            padding: 10px;
            border-radius: 4px;
            margin-bottom: 10px;
            display: none;
        }
    </style>
</head>
<body>
    <div class="container">
        <!-- Login Form -->
        <div id="login-form" class="login-form">
            <h2>Socket.IO Chat Application</h2>
            <div id="error-message" class="error-message"></div>
            <input type="text" id="username-input" placeholder="Enter username" required>
            <button type="submit">Join Chat</button>
        </div>
        
        <!-- Chat Interface -->
        <div id="chat-interface" class="chat-interface">
            <div class="sidebar">
                <div class="rooms-container">
                    <h3>Rooms</h3>
                    <ul id="rooms-list">
                        <!-- Rooms will be populated here -->
                    </ul>
                </div>
                
                <div class="users-container">
                    <h3 id="users-title">Users</h3>
                    <ul id="users-list">
                        <!-- Online users will be populated here -->
                    </ul>
                </div>
            </div>
            
            <div class="chat-container">
                <div class="chat-header">
                    <h2 id="room-title">Room: General</h2>
                </div>
                
                <div id="messages" class="messages">
                    <!-- Messages will be populated here -->
                </div>
                
                <div id="typing-indicator" class="typing-indicator"></div>
                
                <form id="message-form" class="message-form">
                    <input id="message-input" type="text" placeholder="Type a message..." autocomplete="off" required>
                    <button type="submit">Send</button>
                </form>
            </div>
        </div>
    </div>
    
    <script src="/socket.io/socket.io.js"></script>
    <script src="/app.js"></script>
</body>
</html>
                

Collaborative Drawing Application

A simple but powerful example of a real-time collaborative application:


// Server-side for collaborative drawing
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const path = require('path');

const app = express();
const server = http.createServer(app);
const io = new Server(server);

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

// Store drawing history to send to new connections
const drawHistory = [];
const MAX_HISTORY_LENGTH = 1000; // Limit history to prevent memory issues

// Socket.IO connection handling
io.on('connection', (socket) => {
  console.log('New connection:', socket.id);
  
  // Send drawing history to new client
  socket.emit('drawing history', drawHistory);
  
  // Handle drawing events
  socket.on('draw', (data) => {
    // Validate data
    if (!isValidDrawData(data)) return;
    
    // Add to history
    if (drawHistory.length >= MAX_HISTORY_LENGTH) {
      drawHistory.shift(); // Remove oldest item if at max length
    }
    drawHistory.push(data);
    
    // Broadcast to all other clients
    socket.broadcast.emit('draw', data);
  });
  
  // Handle clear canvas event
  socket.on('clear canvas', () => {
    // Clear history
    drawHistory.length = 0;
    
    // Broadcast to all clients
    io.emit('clear canvas');
  });
  
  socket.on('disconnect', () => {
    console.log('Client disconnected:', socket.id);
  });
});

// Validate drawing data to prevent exploits
function isValidDrawData(data) {
  // Check required properties
  if (!data || typeof data !== 'object') return false;
  if (typeof data.x0 !== 'number' || typeof data.y0 !== 'number') return false;
  if (typeof data.x1 !== 'number' || typeof data.y1 !== 'number') return false;
  if (typeof data.color !== 'string') return false;
  if (typeof data.size !== 'number') return false;
  
  // Validate ranges
  if (data.size < 1 || data.size > 50) return false;
  if (!/^#[0-9A-Fa-f]{6}$/.test(data.color)) return false;
  
  return true;
}

// Start the server
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`Drawing server listening on port ${PORT}`);
});
                

// public/app.js - Collaborative drawing client
document.addEventListener('DOMContentLoaded', () => {
  // Canvas setup
  const canvas = document.getElementById('drawing-board');
  const context = canvas.getContext('2d');
  const colorPicker = document.getElementById('color-picker');
  const clearButton = document.getElementById('clear-button');
  const sizeSlider = document.getElementById('size-slider');
  const sizeValue = document.getElementById('size-value');
  const userCount = document.getElementById('user-count');
  
  // Set canvas size
  function resizeCanvas() {
    const container = document.querySelector('.canvas-container');
    canvas.width = container.clientWidth;
    canvas.height = container.clientHeight;
  }
  
  // Initial resize and event listener
  resizeCanvas();
  window.addEventListener('resize', resizeCanvas);
  
  // Drawing state
  let isDrawing = false;
  let lastX = 0;
  let lastY = 0;
  
  // Connect to Socket.IO server
  const socket = io();
  
  // Connection events
  socket.on('connect', () => {
    console.log('Connected to drawing server');
    addSystemMessage('Connected to drawing server');
  });
  
  socket.on('disconnect', () => {
    console.log('Disconnected from drawing server');
    addSystemMessage('Disconnected from server - attempting to reconnect');
  });
  
  socket.on('reconnect', () => {
    console.log('Reconnected to drawing server');
    addSystemMessage('Reconnected to server');
  });
  
  // Update user count when provided by server
  socket.on('user count', (count) => {
    userCount.textContent = count;
  });
  
  // Drawing event listeners
  canvas.addEventListener('mousedown', startDrawing);
  canvas.addEventListener('mousemove', draw);
  canvas.addEventListener('mouseup', stopDrawing);
  canvas.addEventListener('mouseout', stopDrawing);
  
  // Touch support
  canvas.addEventListener('touchstart', handleTouchStart);
  canvas.addEventListener('touchmove', handleTouchMove);
  canvas.addEventListener('touchend', stopDrawing);
  
  // Clear button
  clearButton.addEventListener('click', clearCanvas);
  
  // Size slider
  sizeSlider.addEventListener('input', () => {
    sizeValue.textContent = sizeSlider.value;
  });
  
  // Drawing functions
  function startDrawing(e) {
    isDrawing = true;
    
    // Get correct coordinates
    const rect = canvas.getBoundingClientRect();
    lastX = e.clientX - rect.left;
    lastY = e.clientY - rect.top;
  }
  
  function draw(e) {
    if (!isDrawing) return;
    
    // Get current coordinates
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    
    // Draw line
    drawLine(lastX, lastY, x, y, colorPicker.value, sizeSlider.value);
    
    // Emit drawing event to server
    socket.emit('draw', {
      x0: lastX,
      y0: lastY,
      x1: x,
      y1: y,
      color: colorPicker.value,
      size: parseInt(sizeSlider.value)
    });
    
    // Update last position
    lastX = x;
    lastY = y;
  }
  
  function stopDrawing() {
    isDrawing = false;
  }
  
  function handleTouchStart(e) {
    e.preventDefault();
    
    if (e.touches.length === 1) {
      const touch = e.touches[0];
      const mouseEvent = new MouseEvent('mousedown', {
        clientX: touch.clientX,
        clientY: touch.clientY
      });
      canvas.dispatchEvent(mouseEvent);
    }
  }
  
  function handleTouchMove(e) {
    e.preventDefault();
    
    if (e.touches.length === 1) {
      const touch = e.touches[0];
      const mouseEvent = new MouseEvent('mousemove', {
        clientX: touch.clientX,
        clientY: touch.clientY
      });
      canvas.dispatchEvent(mouseEvent);
    }
  }
  
  function drawLine(x0, y0, x1, y1, color, size) {
    context.beginPath();
    context.moveTo(x0, y0);
    context.lineTo(x1, y1);
    context.strokeStyle = color;
    context.lineWidth = size;
    context.lineCap = 'round';
    context.stroke();
  }
  
  function clearCanvas() {
    context.clearRect(0, 0, canvas.width, canvas.height);
    socket.emit('clear canvas');
  }
  
  // Handle drawing events from other clients
  socket.on('draw', (data) => {
    drawLine(data.x0, data.y0, data.x1, data.y1, data.color, data.size);
  });
  
  // Handle clear canvas event
  socket.on('clear canvas', () => {
    context.clearRect(0, 0, canvas.width, canvas.height);
    addSystemMessage('Canvas cleared by another user');
  });
  
  // Handle drawing history on connection
  socket.on('drawing history', (history) => {
    history.forEach(data => {
      drawLine(data.x0, data.y0, data.x1, data.y1, data.color, data.size);
    });
    
    if (history.length > 0) {
      addSystemMessage('Drawing history loaded');
    }
  });
  
  // Helper: Add system message
  function addSystemMessage(message) {
    const messagesContainer = document.getElementById('messages');
    const messageElement = document.createElement('div');
    messageElement.className = 'system-message';
    messageElement.textContent = message;
    
    messagesContainer.appendChild(messageElement);
    messagesContainer.scrollTop = messagesContainer.scrollHeight;
    
    // Auto-fade messages
    setTimeout(() => {
      messageElement.style.opacity = '0';
      setTimeout(() => {
        messageElement.remove();
      }, 500);
    }, 5000);
  }
});
                

<!-- public/index.html - Collaborative Drawing -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Collaborative Drawing Board</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background-color: #f5f5f5;
            height: 100vh;
            display: flex;
            flex-direction: column;
        }
        
        .header {
            padding: 15px;
            background-color: #4CAF50;
            color: white;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        
        .header h1 {
            font-size: 24px;
        }
        
        .user-info {
            display: flex;
            align-items: center;
        }
        
        .user-count-label {
            margin-right: 5px;
        }
        
        .toolbar {
            padding: 10px;
            background-color: #f0f0f0;
            display: flex;
            align-items: center;
            border-bottom: 1px solid #ddd;
        }
        
        .tool-group {
            margin-right: 20px;
            display: flex;
            align-items: center;
        }
        
        .tool-label {
            margin-right: 10px;
        }
        
        #color-picker {
            width: 40px;
            height: 40px;
            border: none;
            cursor: pointer;
        }
        
        #size-slider {
            width: 100px;
        }
        
        #clear-button {
            padding: 8px 16px;
            background-color: #f44336;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        
        .main-content {
            flex-grow: 1;
            display: flex;
            flex-direction: column;
            position: relative;
        }
        
        .canvas-container {
            flex-grow: 1;
            position: relative;
            overflow: hidden;
        }
        
        #drawing-board {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: white;
            cursor: crosshair;
            box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
        }
        
        #messages {
            position: absolute;
            top: 10px;
            right: 10px;
            width: 250px;
            max-height: 200px;
            overflow-y: auto;
            display: flex;
            flex-direction: column;
            gap: 5px;
        }
        
        .system-message {
            padding: 8px 12px;
            background-color: rgba(0, 0, 0, 0.6);
            color: white;
            border-radius: 4px;
            font-size: 14px;
            opacity: 1;
            transition: opacity 0.5s;
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>Collaborative Drawing Board</h1>
        <div class="user-info">
            <span class="user-count-label">Online Users:</span>
            <span id="user-count">1</span>
        </div>
    </div>
    
    <div class="toolbar">
        <div class="tool-group">
            <label class="tool-label" for="color-picker">Color:</label>
            <input type="color" id="color-picker" value="#000000">
        </div>
        
        <div class="tool-group">
            <label class="tool-label" for="size-slider">Size:</label>
            <input type="range" id="size-slider" min="1" max="50" value="5">
            <span id="size-value">5</span>
        </div>
        
        <button id="clear-button">Clear Canvas</button>
    </div>
    
    <div class="main-content">
        <div class="canvas-container">
            <canvas id="drawing-board"></canvas>
        </div>
        <div id="messages"></div>
    </div>
    
    <script src="/socket.io/socket.io.js"></script>
    <script src="/app.js"></script>
</body>
</html>
                

Real-time Dashboard

A simple example of a real-time data dashboard:


// Server-side for real-time dashboard
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const path = require('path');

const app = express();
const server = http.createServer(app);
const io = new Server(server);

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

// Store connected clients
const connectedClients = new Map();

// Socket.IO connection handling
io.on('connection', (socket) => {
  console.log('Dashboard client connected:', socket.id);
  
  // Store client connection
  connectedClients.set(socket.id, {
    id: socket.id,
    subscriptions: new Set(['system']) // Default subscription to system metrics
  });
  
  // Handle subscription to data feeds
  socket.on('subscribe', (feeds, callback) => {
    if (!Array.isArray(feeds)) {
      return callback?.({
        success: false,
        error: 'Invalid subscription format'
      });
    }
    
    try {
      const client = connectedClients.get(socket.id);
      
      // Add new subscriptions
      feeds.forEach(feed => {
        if (isValidFeed(feed)) {
          client.subscriptions.add(feed);
        }
      });
      
      // Acknowledge successful subscription
      callback?.({
        success: true,
        subscriptions: Array.from(client.subscriptions)
      });
      
      console.log(`Client ${socket.id} subscribed to:`, Array.from(client.subscriptions));
    } catch (error) {
      console.error('Subscription error:', error);
      callback?.({
        success: false,
        error: 'Subscription failed'
      });
    }
  });
  
  // Handle unsubscribe from data feeds
  socket.on('unsubscribe', (feeds, callback) => {
    if (!Array.isArray(feeds)) {
      return callback?.({
        success: false,
        error: 'Invalid unsubscribe format'
      });
    }
    
    try {
      const client = connectedClients.get(socket.id);
      
      // Remove subscriptions
      feeds.forEach(feed => {
        client.subscriptions.delete(feed);
      });
      
      // Acknowledge successful unsubscribe
      callback?.({
        success: true,
        subscriptions: Array.from(client.subscriptions)
      });
      
      console.log(`Client ${socket.id} unsubscribed from feeds, remaining:`, Array.from(client.subscriptions));
    } catch (error) {
      console.error('Unsubscribe error:', error);
      callback?.({
        success: false,
        error: 'Unsubscribe failed'
      });
    }
  });
  
  // Disconnect event
  socket.on('disconnect', () => {
    console.log('Dashboard client disconnected:', socket.id);
    connectedClients.delete(socket.id);
  });
});

// Validate feed name
function isValidFeed(feed) {
  const validFeeds = [
    'system', 'cpu', 'memory', 'network', 
    'disk', 'users', 'errors', 'requests'
  ];
  return validFeeds.includes(feed);
}

// Simulate real-time data generation
function generateDashboardData() {
  // System metrics
  const systemData = {
    type: 'system',
    timestamp: new Date().toISOString(),
    uptime: Math.floor(process.uptime()),
    connectedClients: connectedClients.size
  };
  
  // CPU usage (simulated)
  const cpuData = {
    type: 'cpu',
    timestamp: new Date().toISOString(),
    usage: Math.random() * 100,
    temperature: 40 + Math.random() * 30,
    cores: [
      Math.random() * 100,
      Math.random() * 100,
      Math.random() * 100,
      Math.random() * 100
    ]
  };
  
  // Memory usage (simulated)
  const memoryData = {
    type: 'memory',
    timestamp: new Date().toISOString(),
    used: Math.floor(Math.random() * 16000),
    total: 16000,
    swapUsed: Math.floor(Math.random() * 8000),
    swapTotal: 8000
  };
  
  // Network traffic (simulated)
  const networkData = {
    type: 'network',
    timestamp: new Date().toISOString(),
    bytesIn: Math.floor(Math.random() * 1000000),
    bytesOut: Math.floor(Math.random() * 500000),
    packetsIn: Math.floor(Math.random() * 10000),
    packetsOut: Math.floor(Math.random() * 5000),
    errors: Math.floor(Math.random() * 10)
  };
  
  // Disk usage (simulated)
  const diskData = {
    type: 'disk',
    timestamp: new Date().toISOString(),
    used: Math.floor(Math.random() * 500000),
    total: 500000,
    reads: Math.floor(Math.random() * 1000),
    writes: Math.floor(Math.random() * 800)
  };
  
  // User statistics (simulated)
  const userData = {
    type: 'users',
    timestamp: new Date().toISOString(),
    activeUsers: Math.floor(Math.random() * 1000),
    registrations: Math.floor(Math.random() * 50),
    logins: Math.floor(Math.random() * 200)
  };
  
  // Error rates (simulated)
  const errorData = {
    type: 'errors',
    timestamp: new Date().toISOString(),
    count: Math.floor(Math.random() * 30),
    types: {
      validation: Math.floor(Math.random() * 10),
      authentication: Math.floor(Math.random() * 5),
      database: Math.floor(Math.random() * 8),
      server: Math.floor(Math.random() * 7)
    }
  };
  
  // Request metrics (simulated)
  const requestData = {
    type: 'requests',
    timestamp: new Date().toISOString(),
    total: Math.floor(Math.random() * 5000),
    successful: Math.floor(Math.random() * 4800),
    failed: Math.floor(Math.random() * 200),
    avgResponseTime: Math.random() * 500
  };
  
  // Distribute data to subscribed clients
  connectedClients.forEach((client, clientId) => {
    const socket = io.sockets.sockets.get(clientId);
    
    if (socket) {
      // Send system data to all clients (default subscription)
      socket.emit('dashboard-update', systemData);
      
      // Send other data based on subscriptions
      if (client.subscriptions.has('cpu')) {
        socket.emit('dashboard-update', cpuData);
      }
      
      if (client.subscriptions.has('memory')) {
        socket.emit('dashboard-update', memoryData);
      }
      
      if (client.subscriptions.has('network')) {
        socket.emit('dashboard-update', networkData);
      }
      
      if (client.subscriptions.has('disk')) {
        socket.emit('dashboard-update', diskData);
      }
      
      if (client.subscriptions.has('users')) {
        socket.emit('dashboard-update', userData);
      }
      
      if (client.subscriptions.has('errors')) {
        socket.emit('dashboard-update', errorData);
      }
      
      if (client.subscriptions.has('requests')) {
        socket.emit('dashboard-update', requestData);
      }
    }
  });
}

// Start data generation
const UPDATE_INTERVAL = 2000; // 2 seconds
setInterval(generateDashboardData, UPDATE_INTERVAL);

// Start the server
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`Dashboard server listening on port ${PORT}`);
});
                

// public/dashboard.js
document.addEventListener('DOMContentLoaded', () => {
  // Chart configurations and data storage
  const chartConfigs = {
    cpu: {
      element: 'cpu-chart',
      title: 'CPU Usage',
      data: [],
      maxPoints: 20,
      options: {
        scales: {
          y: {
            beginAtZero: true,
            max: 100,
            title: {
              display: true,
              text: 'Usage (%)'
            }
          }
        }
      }
    },
    memory: {
      element: 'memory-chart',
      title: 'Memory Usage',
      data: [],
      maxPoints: 20,
      options: {
        scales: {
          y: {
            beginAtZero: true,
            title: {
              display: true,
              text: 'Usage (MB)'
            }
          }
        }
      }
    },
    network: {
      element: 'network-chart',
      title: 'Network Traffic',
      data: [],
      maxPoints: 20,
      options: {
        scales: {
          y: {
            beginAtZero: true,
            title: {
              display: true,
              text: 'Traffic (KB/s)'
            }
          }
        }
      }
    },
    requests: {
      element: 'requests-chart',
      title: 'Request Volume',
      data: [],
      maxPoints: 20,
      options: {
        scales: {
          y: {
            beginAtZero: true,
            title: {
              display: true,
              text: 'Requests per minute'
            }
          }
        }
      }
    }
  };
  
  // Initialize charts
  const charts = {};
  
  // Connect to Socket.IO server
  const socket = io();
  
  // Connection events
  socket.on('connect', () => {
    console.log('Connected to dashboard server');
    updateConnectionStatus('Connected', true);
    
    // Subscribe to all data feeds
    socket.emit('subscribe', [
      'cpu', 'memory', 'network', 'disk', 
      'users', 'errors', 'requests'
    ], (response) => {
      if (response.success) {
        console.log('Subscribed to feeds:', response.subscriptions);
      } else {
        console.error('Subscription error:', response.error);
      }
    });
    
    // Initialize charts
    initializeCharts();
  });
  
  socket.on('disconnect', () => {
    console.log('Disconnected from dashboard server');
    updateConnectionStatus('Disconnected', false);
  });
  
  socket.on('reconnect', () => {
    console.log('Reconnected to dashboard server');
    updateConnectionStatus('Connected', true);
  });
  
  // Dashboard data updates
  socket.on('dashboard-update', (data) => {
    // Process data based on type
    switch (data.type) {
      case 'system':
        updateSystemInfo(data);
        break;
      case 'cpu':
        updateChart('cpu', data);
        updateCpuInfo(data);
        break;
      case 'memory':
        updateChart('memory', data);
        updateMemoryInfo(data);
        break;
      case 'network':
        updateChart('network', data);
        updateNetworkInfo(data);
        break;
      case 'disk':
        updateDiskInfo(data);
        break;
      case 'users':
        updateUserInfo(data);
        break;
      case 'errors':
        updateErrorInfo(data);
        break;
      case 'requests':
        updateChart('requests', data);
        updateRequestInfo(data);
        break;
    }
  });
  
  // Toggle panel visibility
  document.querySelectorAll('.panel-header').forEach(header => {
    header.addEventListener('click', () => {
      const panel = header.parentElement;
      panel.classList.toggle('collapsed');
      
      // Resize charts when panel is expanded
      if (!panel.classList.contains('collapsed')) {
        const panelId = panel.id;
        const chartType = panelId.replace('-panel', '');
        
        if (charts[chartType]) {
          setTimeout(() => {
            charts[chartType].resize();
          }, 300);
        }
      }
    });
  });
  
  // Initialize charts
  function initializeCharts() {
    for (const [key, config] of Object.entries(chartConfigs)) {
      const ctx = document.getElementById(config.element).getContext('2d');
      
      charts[key] = new Chart(ctx, {
        type: 'line',
        data: {
          labels: Array(config.maxPoints).fill(''),
          datasets: [{
            label: config.title,
            data: Array(config.maxPoints).fill(null),
            borderColor: getChartColor(key),
            backgroundColor: getChartBackgroundColor(key),
            tension: 0.4,
            fill: true
          }]
        },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          animation: {
            duration: 0
          },
          plugins: {
            legend: {
              display: false
            },
            tooltip: {
              mode: 'index',
              intersect: false
            }
          },
          scales: {
            x: {
              grid: {
                display: false
              }
            },
            ...config.options.scales
          }
        }
      });
    }
  }
  
  // Update chart with new data
  function updateChart(type, data) {
    if (!charts[type]) return;
    
    const chart = charts[type];
    const config = chartConfigs[type];
    const timestamp = new Date(data.timestamp).toLocaleTimeString();
    
    // Add new data point
    config.data.push({
      timestamp,
      value: getValueForChart(type, data)
    });
    
    // Remove old data if exceeding max points
    if (config.data.length > config.maxPoints) {
      config.data.shift();
    }
    
    // Update chart data
    chart.data.labels = config.data.map(point => point.timestamp);
    chart.data.datasets[0].data = config.data.map(point => point.value);
    
    // Update chart
    chart.update();
  }
  
  // Extract value from data for each chart type
  function getValueForChart(type, data) {
    switch (type) {
      case 'cpu':
        return data.usage;
      case 'memory':
        return data.used;
      case 'network':
        return (data.bytesIn + data.bytesOut) / 1024; // Convert to KB
      case 'requests':
        return data.total;
      default:
        return 0;
    }
  }
  
  // Get chart color
  function getChartColor(type) {
    switch (type) {
      case 'cpu': return '#ff6384';
      case 'memory': return '#36a2eb';
      case 'network': return '#4bc0c0';
      case 'requests': return '#9966ff';
      default: return '#ff9f40';
    }
  }
  
  // Get chart background color (transparent version)
  function getChartBackgroundColor(type) {
    switch (type) {
      case 'cpu': return 'rgba(255, 99, 132, 0.2)';
      case 'memory': return 'rgba(54, 162, 235, 0.2)';
      case 'network': return 'rgba(75, 192, 192, 0.2)';
      case 'requests': return 'rgba(153, 102, 255, 0.2)';
      default: return 'rgba(255, 159, 64, 0.2)';
    }
  }
  
  // Update system information
  function updateSystemInfo(data) {
    document.getElementById('uptime-value').textContent = formatUptime(data.uptime);
    document.getElementById('clients-value').textContent = data.connectedClients;
    document.getElementById('last-update-value').textContent = new Date(data.timestamp).toLocaleTimeString();
  }
  
  // Update CPU information
  function updateCpuInfo(data) {
    document.getElementById('cpu-usage-value').textContent = `${data.usage.toFixed(1)}%`;
    document.getElementById('cpu-temperature-value').textContent = `${data.temperature.toFixed(1)}°C`;
    
    // Update CPU cores if element exists
    const coresElement = document.getElementById('cpu-cores-list');
    if (coresElement) {
      coresElement.innerHTML = '';
      
      data.cores.forEach((core, index) => {
        const li = document.createElement('li');
        li.textContent = `Core ${index}: ${core.toFixed(1)}%`;
        
        // Add color based on usage
        if (core > 80) {
          li.classList.add('critical');
        } else if (core > 60) {
          li.classList.add('warning');
        }
        
        coresElement.appendChild(li);
      });
    }
  }
  
  // Update memory information
  function updateMemoryInfo(data) {
    const usedPercent = (data.used / data.total * 100).toFixed(1);
    document.getElementById('memory-usage-value').textContent = `${usedPercent}% (${formatMemory(data.used)} / ${formatMemory(data.total)})`;
    
    const swapPercent = (data.swapUsed / data.swapTotal * 100).toFixed(1);
    document.getElementById('swap-usage-value').textContent = `${swapPercent}% (${formatMemory(data.swapUsed)} / ${formatMemory(data.swapTotal)})`;
    
    // Update progress bars
    updateProgressBar('memory-progress', usedPercent);
    updateProgressBar('swap-progress', swapPercent);
  }
  
  // Update network information
  function updateNetworkInfo(data) {
    document.getElementById('network-in-value').textContent = formatDataSize(data.bytesIn);
    document.getElementById('network-out-value').textContent = formatDataSize(data.bytesOut);
    document.getElementById('network-errors-value').textContent = data.errors;
  }
  
  // Update disk information
  function updateDiskInfo(data) {
    const usedPercent = (data.used / data.total * 100).toFixed(1);
    document.getElementById('disk-usage-value').textContent = `${usedPercent}% (${formatDataSize(data.used)} / ${formatDataSize(data.total)})`;
    document.getElementById('disk-io-value').textContent = `${data.reads} reads, ${data.writes} writes`;
    
    // Update progress bar
    updateProgressBar('disk-progress', usedPercent);
  }
  
  // Update user information
  function updateUserInfo(data) {
    document.getElementById('active-users-value').textContent = data.activeUsers;
    document.getElementById('new-users-value').textContent = data.registrations;
    document.getElementById('logins-value').textContent = data.logins;
  }
  
  // Update error information
  function updateErrorInfo(data) {
    document.getElementById('error-count-value').textContent = data.count;
    
    // Update error types if element exists
    const typesElement = document.getElementById('error-types-list');
    if (typesElement) {
      typesElement.innerHTML = '';
      
      for (const [type, count] of Object.entries(data.types)) {
        const li = document.createElement('li');
        li.textContent = `${type}: ${count}`;
        
        // Add color based on count
        if (count > 5) {
          li.classList.add('critical');
        } else if (count > 2) {
          li.classList.add('warning');
        }
        
        typesElement.appendChild(li);
      }
    }
  }
  
  // Update request information
  function updateRequestInfo(data) {
    document.getElementById('total-requests-value').textContent = data.total;
    document.getElementById('successful-requests-value').textContent = `${data.successful} (${(data.successful / data.total * 100).toFixed(1)}%)`;
    document.getElementById('failed-requests-value').textContent = `${data.failed} (${(data.failed / data.total * 100).toFixed(1)}%)`;
    document.getElementById('response-time-value').textContent = `${data.avgResponseTime.toFixed(2)} ms`;
  }
  
  // Update connection status
  function updateConnectionStatus(status, isConnected) {
    const statusElement = document.getElementById('connection-status');
    statusElement.textContent = status;
    
    if (isConnected) {
      statusElement.classList.remove('disconnected');
      statusElement.classList.add('connected');
    } else {
      statusElement.classList.remove('connected');
      statusElement.classList.add('disconnected');
    }
  }
  
  // Update progress bar
  function updateProgressBar(id, percentage) {
    const progressBar = document.getElementById(id);
    if (!progressBar) return;
    
    progressBar.style.width = `${percentage}%`;
    
    // Update color based on percentage
    if (percentage > 80) {
      progressBar.className = 'progress-bar critical';
    } else if (percentage > 60) {
      progressBar.className = 'progress-bar warning';
    } else {
      progressBar.className = 'progress-bar normal';
    }
  }
  
  // Format uptime
  function formatUptime(seconds) {
    const days = Math.floor(seconds / 86400);
    const hours = Math.floor((seconds % 86400) / 3600);
    const minutes = Math.floor((seconds % 3600) / 60);
    
    if (days > 0) {
      return `${days}d ${hours}h ${minutes}m`;
    } else if (hours > 0) {
      return `${hours}h ${minutes}m`;
    } else {
      return `${minutes}m ${seconds % 60}s`;
    }
  }
  
  // Format memory in MB
  function formatMemory(value) {
    return `${value} MB`;
  }
  
  // Format data size
  function formatDataSize(bytes) {
    if (bytes < 1024) {
      return `${bytes} B`;
    } else if (bytes < 1024 * 1024) {
      return `${(bytes / 1024).toFixed(2)} KB`;
    } else if (bytes < 1024 * 1024 * 1024) {
      return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
    } else {
      return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
    }
  }
});
                

Testing and Debugging Socket.IO Applications

Testing Tools

Debugging Techniques

Common Issues and Solutions

Production Deployment Considerations

Security Best Practices

Scalability

Monitoring and Logging

Deployment Patterns

Practical Exercises

Exercise 1: Basic Chat Application

Create a simple chat application with the following features:

Start with the basic example provided earlier in this lecture and extend it with these features.

Exercise 2: Real-time Collaborative Application

Build a collaborative application of your choice:

Focus on implementing real-time updates and handling concurrent edits correctly.

Exercise 3: Multi-room System

Implement a system that uses Socket.IO's room feature:

Exercise 4: Authentication and Authorization

Enhance a Socket.IO application with security features:

Additional Resources

Summary

Socket.IO is a powerful library that simplifies real-time, bidirectional communication in web applications:

By understanding these concepts and implementing them correctly, you can create responsive, real-time web applications that provide engaging user experiences. Whether you're building chat applications, collaborative tools, dashboards, or multiplayer games, Socket.IO provides the infrastructure needed for modern real-time features.