Skip to main content

Real-Time Communication with WebSockets

Traditional HTTP is request-response based—the client must initiate every interaction. WebSockets enable bidirectional, real-time communication between client and server.

HTTP vs WebSockets

FeatureHTTPWebSockets
ConnectionNew connection per requestPersistent connection
DirectionClient → Server (request/response)Bidirectional
OverheadHeaders sent every timeLow overhead after handshake
Use CaseTraditional web pages, REST APIsChat, gaming, live updates
LatencyHigherVery low

When to Use WebSockets

Perfect for:
  • Chat applications
  • Live notifications
  • Real-time dashboards
  • Multiplayer games
  • Collaborative editing (Google Docs style)
  • Live sports scores
  • Stock tickers
Not needed for:
  • Static websites
  • CRUD operations
  • File uploads
  • SEO-focused content

Socket.io Overview

Socket.io is the most popular WebSocket library for Node.js. It provides:
  • Automatic reconnection
  • Fallback to HTTP long-polling
  • Room and namespace support
  • Broadcasting capabilities
  • Binary data support
npm install socket.io

Basic Setup

Server Side

const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');

const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
  cors: {
    origin: "http://localhost:3000",
    methods: ["GET", "POST"]
  }
});

io.on('connection', (socket) => {
  console.log('User connected:', socket.id);
  
  // Listen for events from client
  socket.on('message', (data) => {
    console.log('Received:', data);
    
    // Send to all clients
    io.emit('message', data);
    
    // Send to all except sender
    socket.broadcast.emit('message', data);
    
    // Send only to sender
    socket.emit('message', data);
  });
  
  socket.on('disconnect', () => {
    console.log('User disconnected:', socket.id);
  });
});

httpServer.listen(3000, () => {
  console.log('Server running on port 3000');
});

Client Side

<script src="/socket.io/socket.io.js"></script>
<script>
  const socket = io('http://localhost:3000');
  
  socket.on('connect', () => {
    console.log('Connected:', socket.id);
  });
  
  socket.on('message', (data) => {
    console.log('Received:', data);
  });
  
  // Send message
  socket.emit('message', 'Hello from client!');
</script>

Building a Chat Application

Server

const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');

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

app.use(express.static('public'));

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

io.on('connection', (socket) => {
  console.log('New connection:', socket.id);

  // User joins with username
  socket.on('join', (username) => {
    users.set(socket.id, { username, id: socket.id });
    
    // Notify all users
    io.emit('user-joined', {
      user: username,
      users: Array.from(users.values())
    });
    
    // Send chat history to new user (in production, fetch from DB)
    socket.emit('chat-history', []);
  });

  // Handle chat messages
  socket.on('chat-message', (data) => {
    const user = users.get(socket.id);
    if (!user) return;
    
    const message = {
      id: Date.now(),
      user: user.username,
      text: data.text,
      timestamp: new Date().toISOString()
    };
    
    // Broadcast to all clients
    io.emit('chat-message', message);
  });

  // Typing indicator
  socket.on('typing', () => {
    const user = users.get(socket.id);
    if (user) {
      socket.broadcast.emit('user-typing', user.username);
    }
  });

  socket.on('stop-typing', () => {
    socket.broadcast.emit('user-stop-typing');
  });

  // User disconnects
  socket.on('disconnect', () => {
    const user = users.get(socket.id);
    users.delete(socket.id);
    
    if (user) {
      io.emit('user-left', {
        user: user.username,
        users: Array.from(users.values())
      });
    }
  });
});

httpServer.listen(3000);

Client (React Example)

import { useEffect, useState, useRef } from 'react';
import io from 'socket.io-client';

function Chat({ username }) {
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');
  const [users, setUsers] = useState([]);
  const [typing, setTyping] = useState('');
  const socketRef = useRef();

  useEffect(() => {
    socketRef.current = io('http://localhost:3000');
    
    socketRef.current.emit('join', username);

    socketRef.current.on('chat-message', (message) => {
      setMessages(prev => [...prev, message]);
    });

    socketRef.current.on('user-joined', ({ users }) => {
      setUsers(users);
    });

    socketRef.current.on('user-left', ({ users }) => {
      setUsers(users);
    });

    socketRef.current.on('user-typing', (user) => {
      setTyping(`${user} is typing...`);
    });

    socketRef.current.on('user-stop-typing', () => {
      setTyping('');
    });

    return () => socketRef.current.disconnect();
  }, [username]);

  const sendMessage = (e) => {
    e.preventDefault();
    if (!input.trim()) return;
    
    socketRef.current.emit('chat-message', { text: input });
    setInput('');
    socketRef.current.emit('stop-typing');
  };

  const handleTyping = (e) => {
    setInput(e.target.value);
    socketRef.current.emit('typing');
    
    // Debounce stop-typing
    clearTimeout(window.typingTimeout);
    window.typingTimeout = setTimeout(() => {
      socketRef.current.emit('stop-typing');
    }, 1000);
  };

  return (
    <div className="chat">
      <div className="users">
        <h3>Online ({users.length})</h3>
        {users.map(u => <div key={u.id}>{u.username}</div>)}
      </div>
      
      <div className="messages">
        {messages.map(msg => (
          <div key={msg.id} className="message">
            <strong>{msg.user}:</strong> {msg.text}
          </div>
        ))}
        {typing && <div className="typing">{typing}</div>}
      </div>
      
      <form onSubmit={sendMessage}>
        <input
          value={input}
          onChange={handleTyping}
          placeholder="Type a message..."
        />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}

Rooms and Namespaces

Rooms

Rooms are arbitrary channels that sockets can join and leave.
io.on('connection', (socket) => {
  // Join a room
  socket.on('join-room', (roomId) => {
    socket.join(roomId);
    socket.to(roomId).emit('user-joined', socket.id);
  });
  
  // Leave a room
  socket.on('leave-room', (roomId) => {
    socket.leave(roomId);
    socket.to(roomId).emit('user-left', socket.id);
  });
  
  // Send to room
  socket.on('room-message', ({ roomId, message }) => {
    io.to(roomId).emit('room-message', message);
  });
});

// Server-side: send to specific room
io.to('room123').emit('notification', 'Hello room!');

// Send to multiple rooms
io.to('room1').to('room2').emit('announcement', 'Hi everyone!');

Namespaces

Namespaces provide separation of concerns.
// Default namespace
io.on('connection', (socket) => {
  // ...
});

// Custom namespaces
const adminNamespace = io.of('/admin');
const chatNamespace = io.of('/chat');

adminNamespace.on('connection', (socket) => {
  console.log('Admin connected');
  // Admin-only events
});

chatNamespace.on('connection', (socket) => {
  console.log('Chat user connected');
  // Chat-specific events
});

// Client connects to namespace
const adminSocket = io('http://localhost:3000/admin');
const chatSocket = io('http://localhost:3000/chat');

Authentication

// Server-side authentication middleware
io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    socket.user = decoded;
    next();
  } catch (err) {
    next(new Error('Authentication failed'));
  }
});

io.on('connection', (socket) => {
  console.log('Authenticated user:', socket.user.id);
});

// Client
const socket = io('http://localhost:3000', {
  auth: {
    token: 'your-jwt-token'
  }
});

socket.on('connect_error', (err) => {
  if (err.message === 'Authentication failed') {
    // Handle auth error
  }
});

Scaling with Redis

For multi-server deployments, use Redis adapter:
npm install @socket.io/redis-adapter redis
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');

const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();

Promise.all([pubClient.connect(), subClient.connect()]).then(() => {
  io.adapter(createAdapter(pubClient, subClient));
  httpServer.listen(3000);
});

Real-Time Notifications System

// Notification service
class NotificationService {
  constructor(io) {
    this.io = io;
    this.userSockets = new Map(); // userId -> socket.id
  }

  registerUser(userId, socketId) {
    this.userSockets.set(userId, socketId);
  }

  unregisterUser(userId) {
    this.userSockets.delete(userId);
  }

  sendToUser(userId, event, data) {
    const socketId = this.userSockets.get(userId);
    if (socketId) {
      this.io.to(socketId).emit(event, data);
    }
  }

  sendToUsers(userIds, event, data) {
    userIds.forEach(userId => this.sendToUser(userId, event, data));
  }

  broadcast(event, data) {
    this.io.emit(event, data);
  }
}

// Usage
const notifications = new NotificationService(io);

io.on('connection', (socket) => {
  socket.on('register', (userId) => {
    notifications.registerUser(userId, socket.id);
  });

  socket.on('disconnect', () => {
    // Find and remove user
  });
});

// Send notification from anywhere in your app
notifications.sendToUser('user123', 'notification', {
  title: 'New message',
  body: 'You have a new message from John'
});

Error Handling

io.on('connection', (socket) => {
  socket.on('message', async (data, callback) => {
    try {
      // Process message
      const result = await processMessage(data);
      
      // Acknowledge success
      if (callback) callback({ success: true, data: result });
    } catch (error) {
      console.error('Message error:', error);
      
      // Send error to client
      if (callback) {
        callback({ success: false, error: error.message });
      } else {
        socket.emit('error', { message: error.message });
      }
    }
  });

  // Global error handler
  socket.on('error', (err) => {
    console.error('Socket error:', err);
  });
});

// Client with acknowledgment
socket.emit('message', data, (response) => {
  if (response.success) {
    console.log('Message sent:', response.data);
  } else {
    console.error('Error:', response.error);
  }
});

Summary

  • WebSockets enable real-time bidirectional communication
  • Socket.io provides a robust abstraction with fallbacks
  • Use rooms for group messaging (chat rooms, channels)
  • Use namespaces to separate different features
  • Implement authentication middleware for security
  • Use Redis adapter for multi-server scaling
  • Handle errors with acknowledgments and try/catch
  • Build typing indicators, presence, and notifications