Skip to main content
โšก Calmops

Building Real-Time Applications with WebSockets

Introduction

WebSockets enable bi-directional, real-time communication between clients and servers. Unlike HTTP requests, connections stay open, allowing instant data exchange. This guide covers building real-time applications with WebSockets.

What Are WebSockets

How WebSockets Work

  1. Client initiates HTTP upgrade request
  2. Server accepts upgrade
  3. WebSocket connection established
  4. Both sides can send messages anytime

WebSocket vs HTTP

Aspect HTTP WebSocket
Direction Request-response Bi-directional
Connection New each request Persistent
Overhead Headers each time Minimal after connect
Use case REST APIs Real-time

Native WebSocket API

Client-Side

const ws = new WebSocket('ws://localhost:3000');

ws.onopen = () => {
  console.log('Connected to server');
  ws.send('Hello, server!');
};

ws.onmessage = (event) => {
  console.log('Message from server:', event.data);
};

ws.onerror = (error) => {
  console.error('WebSocket error:', error);
};

ws.onclose = () => {
  console.log('Connection closed');
};

// Send message
ws.send(JSON.stringify({ type: 'hello', data: 'world' }));

// Close connection
ws.close();

Server-Side (Node.js)

const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 3000 });

wss.on('connection', (ws) => {
  console.log('Client connected');

  ws.on('message', (message) => {
    console.log('Received:', message.toString());
    
    // Echo back
    ws.send(`Server received: ${message}`);
  });

  ws.on('close', () => {
    console.log('Client disconnected');
  });
});

Socket.io

Why Socket.io

  • Fallback support (polling)
  • Automatic reconnection
  • Room-based messaging
  • Event-based API

Server Setup

const { Server } = require('socket.io');

const io = new Server(3000, {
  cors: {
    origin: '*'
  }
});

io.on('connection', (socket) => {
  console.log('User connected:', socket.id);

  // Join room
  socket.on('join-room', (room) => {
    socket.join(room);
    console.log(`Socket ${socket.id} joined room ${room}`);
  });

  // Handle events
  socket.on('chat-message', (data) => {
    // Broadcast to room
    io.to(data.room).emit('new-message', data);
  });

  // Private message
  socket.on('private-message', ({ to, message }) => {
    io.to(to).emit('private-message', {
      from: socket.id,
      message
    });
  });

  socket.on('disconnect', () => {
    console.log('User disconnected');
  });
});

Client Setup

<script src="/socket.io/socket.io.js"></script>
<script>
  const socket = io();

  // Join room
  socket.emit('join-room', 'room-123');

  // Send message
  socket.emit('chat-message', {
    room: 'room-123',
    message: 'Hello everyone!'
  });

  // Listen for messages
  socket.on('new-message', (data) => {
    console.log('New message:', data.message);
  });
</script>

Room Management

Basic Rooms

// Join room
socket.join('room-1');

// Leave room
socket.leave('room-1');

// Send to specific room
io.to('room-1').emit('event', data);

// Send to all rooms except sender
socket.to('room-1').emit('event', data);

Multiple Rooms

// User in multiple rooms
socket.join('user-123');
socket.join('notifications');
socket.join('chat');

// Broadcast to all user's rooms
socket.to(['user-123', 'notifications']).emit('update', {});

Broadcasting Patterns

Broadcast to All

io.emit('broadcast', { message: 'Hello everyone!' });

Broadcast to Room

io.to('room-1').emit('room-message', { text: 'Hello room!' });

Broadcast to Others

// Everyone except sender
socket.broadcast.emit('others', { message: 'Not from me' });

// Everyone in room except sender
socket.to('room-1').emit('others', { message: 'Not from me' });

Scaling WebSockets

Horizontal Scaling

// Using Redis adapter for scaling
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');

const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();

io.adapter(createAdapter(pubClient, subClient));

Sticky Sessions

# Nginx configuration
upstream backend {
  server backend1:3000;
  server backend2:3001;
}

server {
  location /socket.io {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
  }
}

Authentication

Connection Authentication

io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  
  try {
    const user = verifyToken(token);
    socket.user = user;
    next();
  } catch (err) {
    next(new Error('Unauthorized'));
  }
});

io.on('connection', (socket) => {
  console.log('Authenticated user:', socket.user.id);
});

Middleware

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

Error Handling

Connection Errors

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

Reconnection

// Client-side
const socket = io({
  reconnection: true,
  reconnectionAttempts: 5,
  reconnectionDelay: 1000
});

socket.on('reconnect_attempt', (attempt) => {
  console.log('Reconnection attempt:', attempt);
});

socket.on('reconnect', () => {
  console.log('Reconnected!');
});

Practical Examples

Chat Application

// Server
io.on('connection', (socket) => {
  socket.on('join-chat', ({ roomId, userId }) => {
    socket.join(roomId);
    io.to(roomId).emit('user-joined', { userId });
  });

  socket.on('send-message', ({ roomId, message, userId }) => {
    const messageData = {
      id: Date.now(),
      userId,
      message,
      timestamp: new Date()
    };
    io.to(roomId).emit('new-message', messageData);
  });

  socket.on('typing', ({ roomId, userId }) => {
    socket.to(roomId).emit('user-typing', { userId });
  });
});

Live Notifications

// Notification service
function sendNotification(userId, notification) {
  io.to(`user-${userId}`).emit('notification', notification);
}

// User joins their notification room
socket.on('join-notifications', (userId) => {
  socket.join(`user-${userId}`);
});

Best Practices

Security

  • Always authenticate connections
  • Validate message data
  • Rate limit messages
  • Use TLS (wss://)

Performance

  • Don’t store too much in memory
  • Use rooms for scalability
  • Implement heartbeat/ping-pong
  • Clean up disconnected sockets

Architecture

  • Separate WebSocket service
  • Use message queues for reliability
  • Implement persistence for messages

Conclusion

WebSockets enable powerful real-time features. Start simple with native WebSockets, then use Socket.io for production. Remember to plan for scaling and always implement proper authentication.


Resources

Comments