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
- Use meaningful room names: Make them unpredictable for security (e.g., UUIDs for private rooms)
- Clean up on disconnect: Always remove users from tracking when they disconnect
- Validate input: Never trust client data – validate everything
- Limit room sizes: Consider maximum occupancy for performance
- Use namespaces for isolation: Keep unrelated features in separate namespaces
- Implement authentication: Use middleware to verify users before allowing connections
- Handle edge cases: Account for rapid joins/leaves and network issues
- Monitor room state: Keep track of active rooms and user counts
Testing Your Multi-Room Chat
- Start the server:
node server.js
- Open multiple browser tabs
- Create different rooms
- Join users to different rooms
- Test broadcasting within rooms
- Test private messaging
- 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.