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
- Create an Express server with Socket.io
- Implement a canvas on the client side
- Broadcast drawing coordinates in real-time
- Add rooms so multiple groups can draw separately
- Implement a "clear canvas" feature
- Add color selection and brush size controls
- 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.