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.
Why Socket.IO over Raw WebSockets?
- Reliability: Automatic fallbacks when WebSockets aren't available
- Reconnection Support: Automatically handles reconnection with exponential back-off
- Packet Buffering: Stores packets during disconnections and sends them when reconnected
- Broadcasting: Simple APIs for sending messages to multiple clients
- Multiplexing: Namespaces and rooms for organizing connections
- Acknowledgments: Request-response patterns within the WebSocket connection
- Middlewares: Interception points for connection and event handling
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:
- Decoupling: Components only need to know about event names, not each other's implementation
- Flexibility: Multiple listeners can respond to the same event
- Extensibility: New events can be added without changing existing code
- Request-Response: Acknowledgments allow for request-response patterns
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:
- Namespaces: Separate communication channels that share the same connection
- Rooms: Subdivision of namespaces for more fine-grained broadcasting
// 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 = `
`;
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 = `
`;
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
-
Socket.IO Client Test Tool: The Socket.IO admin UI provides a client test tool for debugging connections
// Enable admin UI on server const { Server } = require('socket.io'); const { instrument } = require('@socket.io/admin-ui'); const io = new Server(server); instrument(io, { auth: { type: 'basic', username: 'admin', password: '$2b$10$...' // use bcrypt to hash password }, mode: 'production' // or 'development' }); -
Automated Socket.IO Testing: Use libraries like
socket.io-clientand testing frameworks like Mocha or Jest for unit tests// Example Socket.IO server test const { Server } = require('socket.io'); const { createServer } = require('http'); const { io: Client } = require('socket.io-client'); const { expect } = require('chai'); describe('Socket.IO Server', () => { let io, clientSocket; beforeEach((done) => { const httpServer = createServer(); io = new Server(httpServer); httpServer.listen(() => { const port = httpServer.address().port; clientSocket = Client(`http://localhost:${port}`); clientSocket.on('connect', done); }); }); afterEach(() => { io.close(); clientSocket.close(); }); it('should receive echo message', (done) => { const testMessage = 'Hello World'; // Set up test event handlers io.on('connection', (socket) => { socket.on('echo', (msg) => { socket.emit('echo response', msg); }); }); clientSocket.on('echo response', (msg) => { expect(msg).to.equal(testMessage); done(); }); // Emit test event clientSocket.emit('echo', testMessage); }); }); - Browser DevTools: Chrome and Firefox provide network inspection for WebSocket connections
- Wireshark: For lower-level WebSocket packet inspection
- Artillery: For load testing Socket.IO applications
Debugging Techniques
-
Enable Socket.IO Debug Logs:
// Server-side const io = new Server(server, { debug: true // Enable debug output }); // Client-side // Enable in browser console localStorage.debug = 'socket.io:*'; // Or in Node.js process.env.DEBUG = 'socket.io:*'; -
Event Monitoring: Log all events to identify issues
// Monitor all events on the server io.engine.on('connection', (socket) => { console.log('Transport:', socket.transport.name); // 'polling' or 'websocket' }); io.on('connection', (socket) => { // Log all incoming events socket.onAny((event, ...args) => { console.log(`[${socket.id}] ${event}:`, args); }); // Log all outgoing events const originalEmit = socket.emit; socket.emit = function(...args) { console.log(`[${socket.id}] EMIT ${args[0]}:`, args.slice(1)); return originalEmit.apply(this, args); }; }); -
Socket.IO Servers Extension: For monitoring multiple Socket.IO servers
// server.js const { createAdapter } = require('@socket.io/redis-adapter'); const { setupWorker } = require('@socket.io/sticky'); const cluster = require('cluster'); const http = require('http'); const Redis = require('ioredis'); const express = require('express'); const { Server } = require('socket.io'); const numCPUs = require('os').cpus().length; if (cluster.isMaster) { console.log(`Master ${process.pid} is running`); // Fork workers for (let i = 0; i < numCPUs; i++) { cluster.fork(); } // Create http server const httpServer = http.createServer(); // Set up sticky session setupWorker(httpServer); // Listen httpServer.listen(3000); console.log('Server is running on port 3000'); } else { console.log(`Worker ${process.pid} started`); const app = express(); const server = http.createServer(app); const io = new Server(server); // Redis adapter const pubClient = new Redis(); const subClient = pubClient.duplicate(); // Set up adapter io.adapter(createAdapter(pubClient, subClient)); // Socket.IO connection handling io.on('connection', (socket) => { console.log('Client connected to worker:', process.pid); // Handle socket events socket.on('message', (data) => { io.emit('message', { worker: process.pid, data: data }); }); }); }
Common Issues and Solutions
-
Connection Issues:
- Check CORS configuration
- Verify firewall and network settings
- Check for proxy or load balancer issues
- Ensure WebSocket support is enabled
-
Performance Problems:
- Use Redis adapter for scaling
- Implement message batching for high-frequency events
- Use binary data formats for large payloads
- Monitor memory usage and connection count
-
Disconnection Problems:
- Implement proper reconnection logic
- Check for network timeouts
- Verify proxy configurations
- Adjust pingTimeout and pingInterval settings
Production Deployment Considerations
Security Best Practices
- Always Use TLS (HTTPS/WSS): Encrypt all traffic using TLS certificates
-
Implement Authentication: Secure all connections with proper authentication
// Server-side authentication middleware io.use((socket, next) => { const token = socket.handshake.auth.token; // Verify token verifyToken(token) .then(user => { // Attach user data to socket for future use socket.data.user = user; next(); }) .catch(err => { const error = new Error('Authentication error'); error.data = { details: err.message }; next(error); }); }); // Client-side connection with authentication const socket = io({ auth: { token: "your-auth-token" } }); socket.on('connect_error', (err) => { console.error('Connection error:', err.message); // Handle authentication errors if (err.message === 'Authentication error') { // Redirect to login page window.location.href = '/login'; } }); - Validate All Input: Never trust data received from clients
-
Implement Rate Limiting: Protect against abuse
// Simple rate limiting by socket io.on('connection', (socket) => { // Create rate limiter for this socket const eventTimestamps = new Map(); // Add middleware to track events socket.use((packet, next) => { const [event] = packet; const now = Date.now(); // Skip rate limiting for certain events if (['connect', 'disconnect', 'error'].includes(event)) { return next(); } // Get event history let timestamps = eventTimestamps.get(event) || []; // Clean up old timestamps (older than 1 minute) timestamps = timestamps.filter(time => now - time < 60000); // Check rate limit (e.g., max 10 events per minute) if (timestamps.length >= 10) { return next(new Error('Rate limit exceeded')); } // Add current timestamp timestamps.push(now); eventTimestamps.set(event, timestamps); next(); }); // Handle rate limit errors socket.on('error', (err) => { if (err.message === 'Rate limit exceeded') { socket.emit('rate_limit_exceeded', { message: 'You are sending too many events. Please slow down.' }); } }); }); -
Use Origin Checking: Restrict connections to trusted origins
const io = new Server(server, { cors: { origin: ['https://app.example.com', 'https://admin.example.com'], methods: ['GET', 'POST'], credentials: true } });
Scalability
- Use Sticky Sessions: Ensure clients reconnect to the same server
- Redis Adapter: For multi-server deployments
- Horizontal Scaling: Distribute connections across multiple servers
- Optimized Event Emitting: Use targeted broadcasts, not global events
Monitoring and Logging
- Track Connection Metrics: Monitor connection counts, event rates, etc.
- Application Performance Monitoring (APM): Use tools like New Relic, DataDog, or Elastic APM
- Log Important Events: Connection establishment, disconnections, errors
- Custom Metrics: Track application-specific events and patterns
Deployment Patterns
-
Docker Containerization:
# Dockerfile for Socket.IO server FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . EXPOSE 3000 CMD ["node", "server.js"] -
Kubernetes Deployment:
# socketio-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: socketio-server spec: replicas: 3 selector: matchLabels: app: socketio-server template: metadata: labels: app: socketio-server spec: containers: - name: socketio image: your-registry/socketio-app:latest ports: - containerPort: 3000 env: - name: REDIS_URL value: redis://redis-service:6379 resources: limits: memory: "512Mi" cpu: "500m" requests: memory: "256Mi" cpu: "250m" --- apiVersion: v1 kind: Service metadata: name: socketio-service spec: selector: app: socketio-server ports: - port: 80 targetPort: 3000 type: LoadBalancer -
Zero-Downtime Deployment: Gracefully handle connections during deployments
// Graceful shutdown handling function gracefulShutdown() { console.log('Starting graceful shutdown...'); // Stop accepting new connections server.close(() => { console.log('HTTP server closed'); // Close Socket.IO io.close(() => { console.log('Socket.IO server closed'); // Close Redis connections if using adapter if (pubClient && subClient) { Promise.all([ pubClient.quit(), subClient.quit() ]).then(() => { console.log('Redis connections closed'); process.exit(0); }).catch(err => { console.error('Error closing Redis connections:', err); process.exit(1); }); } else { process.exit(0); } }); }); // Set timeout for force shutdown setTimeout(() => { console.error('Forcing shutdown after timeout'); process.exit(1); }, 30000); // 30 seconds } // Listen for termination signals process.on('SIGTERM', gracefulShutdown); process.on('SIGINT', gracefulShutdown);
Practical Exercises
Exercise 1: Basic Chat Application
Create a simple chat application with the following features:
- User login with username
- Public chat room for all users
- Display of currently online users
- Typing indicators
- Message timestamps and read receipts
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:
- Collaborative drawing board
- Shared to-do list
- Real-time quiz/polling system
- Collaborative text editor
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:
- Allow users to create, join, and leave rooms
- Display room listings and current occupants
- Implement room-specific chat or features
- Add private messaging between users
Exercise 4: Authentication and Authorization
Enhance a Socket.IO application with security features:
- Implement JWT authentication for Socket.IO connections
- Create different user roles with varying permissions
- Secure specific events based on user roles
- Add rate limiting to prevent abuse
Additional Resources
Summary
Socket.IO is a powerful library that simplifies real-time, bidirectional communication in web applications:
- It builds on WebSockets with additional reliability features like fallback transports and reconnection
- The event-based architecture makes it intuitive to use for both simple and complex applications
- Rooms and namespaces provide powerful organizational features for scaling applications
- Middlewares allow for authentication, validation, and other cross-cutting concerns
- Advanced features like acknowledgments, volatile events, and binary support handle specialized use cases
- The library scales well with proper architecture, especially when using the Redis adapter
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.