Real-Time WebSocket Architecture Series: Part 3 – Essential Features (Rooms, Namespaces & Events)

Real-Time WebSocket Architecture Series: Part 3 – Essential Features (Rooms, Namespaces & Events)

This entry is part 3 of 8 in the series Real-Time WebSocket Architecture Series

Welcome to Part 3 of our Real-Time WebSocket Architecture series! In Part 2, we built our first WebSocket server with basic messaging. Now we’ll add powerful features that make Socket.io shine: Rooms, Namespaces, and advanced Event Handling.

These features are essential for building scalable, organized real-time applications like multi-room chat systems, gaming platforms, and collaborative tools.

Understanding the Architecture

Before diving into code, let’s understand how Socket.io organizes connections:

graph TB
    Server[Socket.io Server]
    
    Server --> NS1[Default Namespace '/']
    Server --> NS2[Namespace '/chat']
    Server --> NS3[Namespace '/admin']
    
    NS1 --> R1[All Sockets]
    
    NS2 --> R2[Room: general]
    NS2 --> R3[Room: javascript]
    NS2 --> R4[Room: python]
    
    NS3 --> R5[Room: moderators]
    
    R2 --> U1[User 1]
    R2 --> U2[User 2]
    
    R3 --> U3[User 3]
    R3 --> U4[User 4]
    
    R4 --> U5[User 5]
    
    style Server fill:#667eea,color:#fff
    style NS1 fill:#48bb78,color:#fff
    style NS2 fill:#48bb78,color:#fff
    style NS3 fill:#48bb78,color:#fff
    style R2 fill:#4299e1,color:#fff
    style R3 fill:#4299e1,color:#fff
    style R4 fill:#4299e1,color:#fff

Namespaces: The Top-Level Segmentation

What Are Namespaces?

Namespaces are separate communication channels that allow you to split your application logic over a single shared connection. Think of them as different floors in a building – each floor has its own set of rooms and users.

Key characteristics:

  • Each namespace has its own event handlers
  • Sockets in one namespace cannot directly communicate with another
  • Reduces overhead by multiplexing over a single WebSocket connection
  • Perfect for separating concerns (e.g., chat, notifications, admin)

When to Use Namespaces

  • Different application modules: Separate chat from notifications from admin panel
  • Access control: Restrict certain features to authenticated users
  • Multi-tenancy: Isolate different organizations or workspaces
  • Feature isolation: Keep related functionality together

Creating Namespaces – Server Side

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

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

// Default namespace (always exists)
io.on('connection', (socket) => {
  console.log('User connected to default namespace');
});

// Chat namespace
const chatNamespace = io.of('/chat');
chatNamespace.on('connection', (socket) => {
  console.log(`User ${socket.id} connected to /chat`);
  
  socket.on('send-message', (data) => {
    chatNamespace.emit('receive-message', data);
  });
  
  socket.on('disconnect', () => {
    console.log(`User ${socket.id} disconnected from /chat`);
  });
});

// Admin namespace (restricted)
const adminNamespace = io.of('/admin');
adminNamespace.use((socket, next) => {
  // Authentication middleware
  const token = socket.handshake.auth.token;
  if (isValidAdminToken(token)) {
    next();
  } else {
    next(new Error('Authentication failed'));
  }
});

adminNamespace.on('connection', (socket) => {
  console.log('Admin connected');
  
  socket.on('admin-action', (data) => {
    // Admin-specific actions
    adminNamespace.emit('admin-update', data);
  });
});

// Notifications namespace
const notifNamespace = io.of('/notifications');
notifNamespace.on('connection', (socket) => {
  socket.on('subscribe', (userId) => {
    socket.join(`user-${userId}`);
  });
});

server.listen(3000);

Connecting to Namespaces – Client Side

// Connect to different namespaces
const defaultSocket = io(); // or io('/')
const chatSocket = io('/chat');
const adminSocket = io('/admin', {
  auth: {
    token: 'admin-token-here'
  }
});
const notifSocket = io('/notifications');

// Each namespace has independent event handlers
chatSocket.on('receive-message', (data) => {
  console.log('Chat message:', data);
});

notifSocket.on('notification', (data) => {
  console.log('New notification:', data);
});

adminSocket.on('connect_error', (err) => {
  console.log('Admin auth failed:', err.message);
});

Rooms: Grouping Within Namespaces

What Are Rooms?

Rooms are arbitrary channels within a namespace that sockets can join and leave. They’re server-side only constructs – clients don’t know which room they’re in.

Key characteristics:

  • Lightweight and dynamic (created on-demand)
  • Sockets can be in multiple rooms simultaneously
  • Perfect for targeted broadcasting
  • Automatically cleaned up when empty

When to Use Rooms

  • Chat channels: Different topics or conversation groups
  • Game lobbies: Players in the same match
  • Collaborative docs: Users editing the same document
  • Private messaging: One-to-one conversations
  • User-specific updates: Broadcast to all sessions of a user

Complete Multi-Room Chat Implementation

Let’s build a feature-rich multi-room chat system:

// server.js - Multi-room Chat Server
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);

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

// Store users and rooms
const users = new Map(); // socket.id => {username, currentRoom}
const rooms = new Map(); // roomName => Set of socket.ids

io.on('connection', (socket) => {
  console.log(`User connected: ${socket.id}`);
  
  // User joins with username
  socket.on('join-server', (username) => {
    users.set(socket.id, {
      username: username,
      currentRoom: null
    });
    
    // Send available rooms list
    socket.emit('rooms-list', Array.from(rooms.keys()));
  });
  
  // Join a specific room
  socket.on('join-room', (roomName) => {
    const user = users.get(socket.id);
    if (!user) return;
    
    // Leave current room if in one
    if (user.currentRoom) {
      socket.leave(user.currentRoom);
      io.to(user.currentRoom).emit('user-left', {
        username: user.username,
        room: user.currentRoom
      });
      
      // Remove from room tracking
      const roomUsers = rooms.get(user.currentRoom);
      if (roomUsers) {
        roomUsers.delete(socket.id);
        if (roomUsers.size === 0) {
          rooms.delete(user.currentRoom);
        }
      }
    }
    
    // Join new room
    socket.join(roomName);
    user.currentRoom = roomName;
    
    // Track room membership
    if (!rooms.has(roomName)) {
      rooms.set(roomName, new Set());
    }
    rooms.get(roomName).add(socket.id);
    
    // Notify room members
    socket.to(roomName).emit('user-joined', {
      username: user.username,
      room: roomName
    });
    
    // Send room info to user
    const roomUsers = Array.from(rooms.get(roomName))
      .map(id => users.get(id)?.username)
      .filter(Boolean);
      
    socket.emit('room-joined', {
      room: roomName,
      users: roomUsers
    });
    
    console.log(`${user.username} joined ${roomName}`);
  });
  
  // Send message to current room
  socket.on('room-message', (message) => {
    const user = users.get(socket.id);
    if (!user || !user.currentRoom) return;
    
    io.to(user.currentRoom).emit('room-message', {
      username: user.username,
      message: message,
      room: user.currentRoom,
      timestamp: new Date().toISOString()
    });
  });
  
  // Private message to specific user
  socket.on('private-message', ({ targetUsername, message }) => {
    const sender = users.get(socket.id);
    if (!sender) return;
    
    // Find target socket
    for (const [socketId, userData] of users) {
      if (userData.username === targetUsername) {
        io.to(socketId).emit('private-message', {
          from: sender.username,
          message: message,
          timestamp: new Date().toISOString()
        });
        
        // Confirm to sender
        socket.emit('private-message-sent', {
          to: targetUsername,
          message: message
        });
        break;
      }
    }
  });
  
  // Create new room
  socket.on('create-room', (roomName) => {
    if (!rooms.has(roomName)) {
      rooms.set(roomName, new Set());
      io.emit('room-created', roomName);
      console.log(`Room created: ${roomName}`);
    }
  });
  
  // Get online users in current room
  socket.on('get-room-users', () => {
    const user = users.get(socket.id);
    if (!user || !user.currentRoom) return;
    
    const roomUsers = Array.from(rooms.get(user.currentRoom) || [])
      .map(id => users.get(id)?.username)
      .filter(Boolean);
      
    socket.emit('room-users', {
      room: user.currentRoom,
      users: roomUsers
    });
  });
  
  // Typing indicator for room
  socket.on('typing-start', () => {
    const user = users.get(socket.id);
    if (!user || !user.currentRoom) return;
    
    socket.to(user.currentRoom).emit('user-typing', {
      username: user.username,
      room: user.currentRoom
    });
  });
  
  socket.on('typing-stop', () => {
    const user = users.get(socket.id);
    if (!user || !user.currentRoom) return;
    
    socket.to(user.currentRoom).emit('user-stopped-typing', {
      username: user.username,
      room: user.currentRoom
    });
  });
  
  // Disconnect handling
  socket.on('disconnect', () => {
    const user = users.get(socket.id);
    if (!user) return;
    
    // Notify current room
    if (user.currentRoom) {
      socket.to(user.currentRoom).emit('user-left', {
        username: user.username,
        room: user.currentRoom
      });
      
      // Clean up room tracking
      const roomUsers = rooms.get(user.currentRoom);
      if (roomUsers) {
        roomUsers.delete(socket.id);
        if (roomUsers.size === 0) {
          rooms.delete(user.currentRoom);
          io.emit('room-deleted', user.currentRoom);
        }
      }
    }
    
    users.delete(socket.id);
    console.log(`${user.username} disconnected`);
  });
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

Room Communication Patterns

sequenceDiagram
    participant U1 as User 1
    participant Server
    participant Room as Room "javascript"
    participant U2 as User 2
    participant U3 as User 3
    
    U1->>Server: join-room("javascript")
    Server->>Room: socket.join()
    Server->>U2: user-joined event
    Server->>U3: user-joined event
    Server->>U1: room-joined (with user list)
    
    U1->>Server: room-message("Hello!")
    Server->>Room: io.to("javascript").emit()
    Room->>U1: room-message
    Room->>U2: room-message
    Room->>U3: room-message
    
    U1->>Server: private-message to User 2
    Server->>U2: private-message (only)
    
    U1->>Server: disconnect
    Server->>Room: Leave room
    Server->>U2: user-left event
    Server->>U3: user-left event

Broadcasting Strategies

Understanding different broadcasting methods is crucial:

// 1. Broadcast to ALL clients (including sender)
io.emit('event-name', data);

// 2. Broadcast to ALL except sender
socket.broadcast.emit('event-name', data);

// 3. Send to specific socket
io.to(socketId).emit('event-name', data);

// 4. Broadcast to room (including sender)
io.to('room-name').emit('event-name', data);

// 5. Broadcast to room (excluding sender)
socket.to('room-name').emit('event-name', data);

// 6. Broadcast to multiple rooms
io.to('room1').to('room2').emit('event-name', data);

// 7. Broadcast to room except certain sockets
socket.to('room-name').except(socketId).emit('event-name', data);

// 8. Broadcast within namespace
io.of('/chat').emit('event-name', data);

Advanced Event Handling

Event Acknowledgments

Get confirmation that events were received:

// Server
socket.on('important-action', (data, callback) => {
  // Process action
  const result = processAction(data);
  
  // Acknowledge with result
  callback({
    success: true,
    result: result
  });
});

// Client
socket.emit('important-action', data, (response) => {
  if (response.success) {
    console.log('Action confirmed:', response.result);
  }
});

Middleware for Namespaces

// Authentication middleware
io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  if (isValid(token)) {
    socket.userId = getUserIdFromToken(token);
    next();
  } else {
    next(new Error('Authentication error'));
  }
});

// Logging middleware
io.use((socket, next) => {
  console.log(`Connection attempt from ${socket.handshake.address}`);
  next();
});

// Rate limiting middleware
const rateLimiter = new Map();
io.use((socket, next) => {
  const ip = socket.handshake.address;
  const now = Date.now();
  const requests = rateLimiter.get(ip) || [];
  
  const recentRequests = requests.filter(time => now - time < 60000);
  
  if (recentRequests.length > 100) {
    next(new Error('Too many requests'));
  } else {
    recentRequests.push(now);
    rateLimiter.set(ip, recentRequests);
    next();
  }
});

Error Handling

// Server-side error handling
socket.on('error', (error) => {
  console.error('Socket error:', error);
});

// Handle specific errors
socket.on('some-event', (data) => {
  try {
    // Process data
  } catch (error) {
    socket.emit('error', {
      message: 'Failed to process request',
      code: 'PROCESSING_ERROR'
    });
  }
});

// Client-side error handling
socket.on('connect_error', (error) => {
  console.error('Connection failed:', error.message);
});

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

Best Practices

  1. Use meaningful room names: Make them unpredictable for security (e.g., UUIDs for private rooms)
  2. Clean up on disconnect: Always remove users from tracking when they disconnect
  3. Validate input: Never trust client data – validate everything
  4. Limit room sizes: Consider maximum occupancy for performance
  5. Use namespaces for isolation: Keep unrelated features in separate namespaces
  6. Implement authentication: Use middleware to verify users before allowing connections
  7. Handle edge cases: Account for rapid joins/leaves and network issues
  8. Monitor room state: Keep track of active rooms and user counts

Testing Your Multi-Room Chat

  1. Start the server: node server.js
  2. Open multiple browser tabs
  3. Create different rooms
  4. Join users to different rooms
  5. Test broadcasting within rooms
  6. Test private messaging
  7. Test user leaving and rejoining

What’s Next

In Part 4: Authentication & Security, we’ll implement JWT authentication, session management, input validation, and rate limiting to make our application production-ready!


Part 3 of the 8-part Real-Time WebSocket Architecture Series.

Navigate<< Real-Time WebSocket Architecture Series: Part 2 – Building Your First WebSocket Server (Node.js)Real-Time WebSocket Architecture Series: Part 4 – Authentication & Security >>

Written by:

373 Posts

View All Posts
Follow Me :