Node.js & Express

Real-time Communication with Socket.io

45 min Lesson 21 of 40

Real-time Communication with Socket.io

Socket.io is a powerful library that enables real-time, bidirectional, and event-based communication between the browser and the server. It builds on top of WebSockets and provides fallback mechanisms for older browsers, making it the go-to solution for real-time features like chat applications, live notifications, collaborative editing, and real-time dashboards.

Understanding WebSockets

Before diving into Socket.io, let's understand WebSockets:

WebSocket vs HTTP: Traditional HTTP is request-response based - the client requests, the server responds, and the connection closes. WebSockets establish a persistent, full-duplex connection where both client and server can send messages at any time without waiting for a request.

WebSocket Benefits:

  • Real-time bidirectional communication
  • Lower latency compared to polling
  • Reduced server load (no repeated HTTP handshakes)
  • Perfect for chat, gaming, live updates, and collaborative tools

Installing and Setting Up Socket.io

Install Socket.io in your Express application:

npm install socket.io

Basic Express + Socket.io Server:

// server.js const express = require('express'); const http = require('http'); const socketIo = require('socket.io'); const app = express(); const server = http.createServer(app); const io = socketIo(server, { cors: { origin: "http://localhost:3000", methods: ["GET", "POST"] } }); // Serve static files app.use(express.static('public')); // Socket.io connection event io.on('connection', (socket) => { console.log('New client connected:', socket.id); // Listen for custom events socket.on('message', (data) => { console.log('Message received:', data); // Broadcast to all clients io.emit('message', data); }); // Handle disconnection socket.on('disconnect', () => { console.log('Client disconnected:', socket.id); }); }); const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });
Important: Use http.createServer(app) and pass it to Socket.io. Don't use app.listen() directly when integrating Socket.io with Express.

Client-Side Socket.io Setup

Create a basic HTML client:

<!-- 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: 8px; margin-bottom: 5px; background: #f0f0f0; } </style> </head> <body> <h1>Real-time Chat</h1> <ul id="messages"></ul> <form id="messageForm"> <input id="messageInput" autocomplete="off" /> <button>Send</button> </form> <script src="/socket.io/socket.io.js"></script> <script> const socket = io(); // Listen for messages from server socket.on('message', (data) => { const item = document.createElement('li'); item.textContent = data; document.getElementById('messages').appendChild(item); window.scrollTo(0, document.body.scrollHeight); }); // Send message on form submit document.getElementById('messageForm').addEventListener('submit', (e) => { e.preventDefault(); const input = document.getElementById('messageInput'); if (input.value) { socket.emit('message', input.value); input.value = ''; } }); </script> </body> </html>

Socket.io Events

Socket.io uses custom events for communication. You can emit and listen to any event name:

// Server-side io.on('connection', (socket) => { // Listen to custom events socket.on('chat message', (msg) => { console.log('Chat message:', msg); }); socket.on('user typing', (username) => { socket.broadcast.emit('user typing', username); }); socket.on('user stopped typing', (username) => { socket.broadcast.emit('user stopped typing', username); }); // Send event to specific client socket.emit('welcome', 'Welcome to the chat!'); // Send to all clients including sender io.emit('user joined', 'A user has joined'); // Send to all clients except sender socket.broadcast.emit('user joined', 'A user has joined'); });

Client-side event handling:

// Client-side const socket = io(); // Emit events socket.emit('chat message', 'Hello World!'); socket.emit('user typing', 'John'); // Listen to events socket.on('welcome', (message) => { console.log(message); }); socket.on('user joined', (message) => { console.log(message); }); socket.on('user typing', (username) => { console.log(`${username} is typing...`); });

Rooms and Namespaces

Rooms allow you to group sockets together and broadcast messages to specific groups:

// Join a room socket.join('room1'); // Leave a room socket.leave('room1'); // Send to all clients in a room io.to('room1').emit('message', 'Hello room1!'); // Send to multiple rooms io.to('room1').to('room2').emit('message', 'Hello both rooms!'); // Get all rooms a socket is in console.log(socket.rooms); // Example: Chat room implementation io.on('connection', (socket) => { socket.on('join room', (roomName) => { socket.join(roomName); socket.to(roomName).emit('user joined', `User ${socket.id} joined ${roomName}`); }); socket.on('leave room', (roomName) => { socket.leave(roomName); socket.to(roomName).emit('user left', `User ${socket.id} left ${roomName}`); }); socket.on('room message', ({ room, message }) => { io.to(room).emit('room message', { user: socket.id, message: message, timestamp: Date.now() }); }); });
Tip: Every socket automatically joins a room identified by its socket.id. You can use io.to(socket.id).emit() to send messages to a specific socket.

Namespaces allow you to split your application logic across different endpoints:

// Server-side - create namespaces const chatNamespace = io.of('/chat'); const adminNamespace = io.of('/admin'); chatNamespace.on('connection', (socket) => { console.log('User connected to chat namespace'); socket.on('chat message', (msg) => { chatNamespace.emit('chat message', msg); }); }); adminNamespace.on('connection', (socket) => { console.log('Admin connected to admin namespace'); socket.on('admin command', (command) => { adminNamespace.emit('command result', `Executed: ${command}`); }); }); // Client-side - connect to specific namespace const chatSocket = io('http://localhost:3000/chat'); const adminSocket = io('http://localhost:3000/admin'); chatSocket.on('chat message', (msg) => { console.log('Chat:', msg); }); adminSocket.on('command result', (result) => { console.log('Admin:', result); });

Broadcasting Messages

Socket.io provides multiple ways to broadcast messages:

io.on('connection', (socket) => { // Send to current socket only socket.emit('message', 'Just to you'); // Send to all clients including sender io.emit('message', 'To everyone'); // Send to all clients except sender socket.broadcast.emit('message', 'To everyone except sender'); // Send to all clients in room1 except sender socket.to('room1').emit('message', 'To room1 except sender'); // Send to all clients in room1 including sender io.to('room1').emit('message', 'To room1 including sender'); // Send to all clients in namespace io.of('/chat').emit('message', 'To all in /chat namespace'); // Send to specific socket by ID io.to(socketId).emit('message', 'To specific socket'); // Chain multiple rooms socket.to('room1').to('room2').to('room3').emit('message', 'To multiple rooms'); });

Building a Complete Chat Application

Here's a production-ready chat application with rooms, typing indicators, and user management:

// chat-server.js const express = require('express'); const http = require('http'); const socketIo = require('socket.io'); const app = express(); const server = http.createServer(app); const io = socketIo(server); app.use(express.static('public')); // Store active users const users = new Map(); const rooms = new Map(); io.on('connection', (socket) => { console.log('New connection:', socket.id); // Handle user joining socket.on('join', ({ username, room }) => { // Store user info users.set(socket.id, { username, room }); socket.join(room); // Track room members if (!rooms.has(room)) { rooms.set(room, new Set()); } rooms.get(room).add(socket.id); // Welcome message to user socket.emit('message', { user: 'System', text: `Welcome to ${room}, ${username}!`, timestamp: Date.now() }); // Notify room about new user socket.to(room).emit('message', { user: 'System', text: `${username} has joined the room`, timestamp: Date.now() }); // Send updated user list io.to(room).emit('room users', { room, users: Array.from(rooms.get(room)).map(id => users.get(id)?.username) }); }); // Handle chat messages socket.on('chat message', (message) => { const user = users.get(socket.id); if (user) { io.to(user.room).emit('message', { user: user.username, text: message, timestamp: Date.now() }); } }); // Handle typing indicator socket.on('typing', () => { const user = users.get(socket.id); if (user) { socket.to(user.room).emit('user typing', user.username); } }); socket.on('stop typing', () => { const user = users.get(socket.id); if (user) { socket.to(user.room).emit('user stopped typing', user.username); } }); // Handle disconnection socket.on('disconnect', () => { const user = users.get(socket.id); if (user) { const { username, room } = user; // Remove from room if (rooms.has(room)) { rooms.get(room).delete(socket.id); if (rooms.get(room).size === 0) { rooms.delete(room); } } // Remove user users.delete(socket.id); // Notify room io.to(room).emit('message', { user: 'System', text: `${username} has left the room`, timestamp: Date.now() }); // Update user list if (rooms.has(room)) { io.to(room).emit('room users', { room, users: Array.from(rooms.get(room)).map(id => users.get(id)?.username) }); } } }); }); const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log(`Chat server running on port ${PORT}`); });

Socket.io Middleware

Use middleware for authentication and authorization:

// Authentication middleware io.use((socket, next) => { const token = socket.handshake.auth.token; if (!token) { return next(new Error('Authentication required')); } // Verify token (example) try { const decoded = jwt.verify(token, process.env.JWT_SECRET); socket.user = decoded; next(); } catch (err) { next(new Error('Invalid token')); } }); // Logging middleware io.use((socket, next) => { console.log('Socket connection attempt:', { id: socket.id, ip: socket.handshake.address, timestamp: new Date() }); next(); }); // Client-side with authentication const socket = io({ auth: { token: 'your-jwt-token' } }); // Handle connection errors socket.on('connect_error', (err) => { console.error('Connection failed:', err.message); });

Exercise: Build a Real-time Collaborative Drawing App

  1. Create an Express server with Socket.io
  2. Implement a canvas on the client side
  3. Broadcast drawing coordinates in real-time
  4. Add rooms so multiple groups can draw separately
  5. Implement a "clear canvas" feature
  6. Add color selection and brush size controls
  7. Show active users in each room

Performance and Best Practices

Best Practices:
  • Use rooms to limit message broadcasting scope
  • Implement acknowledgments for critical messages
  • Set up heartbeat/ping mechanisms for connection monitoring
  • Use binary data for sending images/files
  • Implement rate limiting to prevent message flooding
  • Clean up event listeners on disconnect
  • Use namespaces to separate application concerns
// Acknowledgments for reliable delivery socket.emit('important message', data, (response) => { console.log('Server acknowledged:', response); }); // Server-side acknowledgment socket.on('important message', (data, callback) => { // Process data callback({ status: 'received', timestamp: Date.now() }); }); // Rate limiting example const messageRates = new Map(); socket.on('chat message', (msg) => { const now = Date.now(); const userRate = messageRates.get(socket.id) || { count: 0, resetTime: now + 60000 }; if (now > userRate.resetTime) { userRate.count = 0; userRate.resetTime = now + 60000; } if (userRate.count >= 10) { socket.emit('rate limit', 'Too many messages. Please slow down.'); return; } userRate.count++; messageRates.set(socket.id, userRate); // Process message io.emit('chat message', msg); });

Socket.io makes building real-time features straightforward and reliable. It handles reconnection, fallbacks, and cross-browser compatibility automatically, allowing you to focus on building amazing real-time experiences.