WebSocket in Practice: Building Real-Time Communication Apps

Master WebSocket protocol, Socket.IO, message pushing and real-time data sync

WebSocket in Practice: Building Real-Time Communication Apps

Real-time communication is a core requirement for modern web applications. This article explores WebSocket protocol and real-time application development practices.

Real-Time Communication Technologies

Technology Comparison

Real-Time Communication Technologies:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   Polling                                           │
│   ├── Timed requests                               │
│   ├── Simple implementation                        │
│   └── High resource consumption, high latency     │
│                                                     │
│   Long Polling                                      │
│   ├── Server holds connection until data ready    │
│   ├── Reduces unnecessary requests                │
│   └── Frequent connection open/close              │
│                                                     │
│   SSE (Server-Sent Events)                          │
│   ├── One-way (server to client)                  │
│   ├── Based on HTTP                               │
│   └── Auto-reconnect                              │
│                                                     │
│   WebSocket                                         │
│   ├── Full-duplex communication                   │
│   ├── Low latency                                  │
│   └── Requires dedicated server support           │
│                                                     │
└─────────────────────────────────────────────────────┘
TechnologyLatencyBandwidthComplexityUse Case
PollingHighHighLowSimple updates
Long PollingMediumMediumMediumNotifications
SSELowLowLowData streaming
WebSocketVery LowVery LowHighBidirectional

WebSocket Basics

Native WebSocket

// Server (Node.js + ws)
import { WebSocketServer, WebSocket } from 'ws';
import http from 'http';

const server = http.createServer();
const wss = new WebSocketServer({ server });

// Connection management
const clients = new Map<string, WebSocket>();

wss.on('connection', (ws, req) => {
  const clientId = generateClientId();
  clients.set(clientId, ws);

  console.log(`Client ${clientId} connected`);

  // Send welcome message
  ws.send(JSON.stringify({
    type: 'welcome',
    clientId,
  }));

  // Receive messages
  ws.on('message', (data) => {
    try {
      const message = JSON.parse(data.toString());
      handleMessage(clientId, message);
    } catch (error) {
      console.error('Invalid message format');
    }
  });

  // Connection closed
  ws.on('close', () => {
    clients.delete(clientId);
    console.log(`Client ${clientId} disconnected`);
  });

  // Error handling
  ws.on('error', (error) => {
    console.error(`Client ${clientId} error:`, error);
  });

  // Heartbeat
  ws.isAlive = true;
  ws.on('pong', () => {
    ws.isAlive = true;
  });
});

// Heartbeat timer
const heartbeat = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (!ws.isAlive) {
      return ws.terminate();
    }
    ws.isAlive = false;
    ws.ping();
  });
}, 30000);

wss.on('close', () => {
  clearInterval(heartbeat);
});

server.listen(8080);

Client Connection

// Client
class WebSocketClient {
  private ws: WebSocket | null = null;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 5;
  private reconnectDelay = 1000;
  private messageHandlers = new Map<string, (data: any) => void>();

  constructor(private url: string) {}

  connect(): Promise<void> {
    return new Promise((resolve, reject) => {
      this.ws = new WebSocket(this.url);

      this.ws.onopen = () => {
        console.log('WebSocket connected');
        this.reconnectAttempts = 0;
        resolve();
      };

      this.ws.onmessage = (event) => {
        const message = JSON.parse(event.data);
        const handler = this.messageHandlers.get(message.type);
        if (handler) {
          handler(message.data);
        }
      };

      this.ws.onclose = () => {
        console.log('WebSocket disconnected');
        this.attemptReconnect();
      };

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

  private attemptReconnect(): void {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('Max reconnection attempts reached');
      return;
    }

    this.reconnectAttempts++;
    const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);

    setTimeout(() => {
      console.log(`Reconnecting... (attempt ${this.reconnectAttempts})`);
      this.connect();
    }, delay);
  }

  on(type: string, handler: (data: any) => void): void {
    this.messageHandlers.set(type, handler);
  }

  send(type: string, data: any): void {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({ type, data }));
    }
  }

  close(): void {
    this.ws?.close();
  }
}

// Usage
const client = new WebSocketClient('ws://localhost:8080');
await client.connect();

client.on('welcome', (data) => {
  console.log('Connected with ID:', data.clientId);
});

client.on('message', (data) => {
  console.log('Received:', data);
});

client.send('chat', { text: 'Hello!' });

Socket.IO in Practice

Server Configuration

import { Server } from 'socket.io';
import { createServer } from 'http';
import express from 'express';

const app = express();
const httpServer = createServer(app);

const io = new Server(httpServer, {
  cors: {
    origin: ['http://localhost:3000'],
    methods: ['GET', 'POST'],
    credentials: true,
  },
  pingTimeout: 60000,
  pingInterval: 25000,
});

// Authentication middleware
io.use(async (socket, next) => {
  const token = socket.handshake.auth.token;

  try {
    const user = await verifyToken(token);
    socket.data.user = user;
    next();
  } catch (error) {
    next(new Error('Authentication failed'));
  }
});

// Connection handling
io.on('connection', (socket) => {
  const user = socket.data.user;
  console.log(`User ${user.id} connected`);

  // Join user room
  socket.join(`user:${user.id}`);

  // Chat messages
  socket.on('chat:send', async (data, callback) => {
    try {
      const message = await saveMessage({
        senderId: user.id,
        roomId: data.roomId,
        content: data.content,
      });

      // Broadcast to room
      io.to(data.roomId).emit('chat:message', message);

      callback({ success: true, messageId: message.id });
    } catch (error) {
      callback({ success: false, error: error.message });
    }
  });

  // Join room
  socket.on('room:join', (roomId) => {
    socket.join(roomId);
    socket.to(roomId).emit('room:user-joined', {
      userId: user.id,
      username: user.name,
    });
  });

  // Leave room
  socket.on('room:leave', (roomId) => {
    socket.leave(roomId);
    socket.to(roomId).emit('room:user-left', {
      userId: user.id,
    });
  });

  // Typing status
  socket.on('typing:start', (roomId) => {
    socket.to(roomId).emit('typing:user', {
      userId: user.id,
      username: user.name,
    });
  });

  socket.on('typing:stop', (roomId) => {
    socket.to(roomId).emit('typing:stopped', {
      userId: user.id,
    });
  });

  // Disconnect
  socket.on('disconnect', (reason) => {
    console.log(`User ${user.id} disconnected: ${reason}`);
  });
});

httpServer.listen(3001);

Client Integration

import { io, Socket } from 'socket.io-client';

class ChatClient {
  private socket: Socket;
  private eventHandlers = new Map<string, Set<Function>>();

  constructor(url: string, token: string) {
    this.socket = io(url, {
      auth: { token },
      autoConnect: false,
      reconnection: true,
      reconnectionAttempts: 5,
      reconnectionDelay: 1000,
    });

    this.setupListeners();
  }

  private setupListeners(): void {
    this.socket.on('connect', () => {
      console.log('Connected to chat server');
      this.emit('connected');
    });

    this.socket.on('disconnect', (reason) => {
      console.log('Disconnected:', reason);
      this.emit('disconnected', reason);
    });

    this.socket.on('connect_error', (error) => {
      console.error('Connection error:', error.message);
      this.emit('error', error);
    });

    this.socket.on('chat:message', (message) => {
      this.emit('message', message);
    });

    this.socket.on('typing:user', (data) => {
      this.emit('typing', data);
    });
  }

  connect(): void {
    this.socket.connect();
  }

  disconnect(): void {
    this.socket.disconnect();
  }

  joinRoom(roomId: string): void {
    this.socket.emit('room:join', roomId);
  }

  leaveRoom(roomId: string): void {
    this.socket.emit('room:leave', roomId);
  }

  sendMessage(roomId: string, content: string): Promise<{ messageId: string }> {
    return new Promise((resolve, reject) => {
      this.socket.emit('chat:send', { roomId, content }, (response) => {
        if (response.success) {
          resolve({ messageId: response.messageId });
        } else {
          reject(new Error(response.error));
        }
      });
    });
  }

  startTyping(roomId: string): void {
    this.socket.emit('typing:start', roomId);
  }

  stopTyping(roomId: string): void {
    this.socket.emit('typing:stop', roomId);
  }

  on(event: string, handler: Function): void {
    if (!this.eventHandlers.has(event)) {
      this.eventHandlers.set(event, new Set());
    }
    this.eventHandlers.get(event)!.add(handler);
  }

  private emit(event: string, data?: any): void {
    const handlers = this.eventHandlers.get(event);
    handlers?.forEach(handler => handler(data));
  }
}

Rooms and Broadcasting

// Advanced broadcast patterns
class BroadcastService {
  constructor(private io: Server) {}

  // Send to specific user
  toUser(userId: string, event: string, data: any): void {
    this.io.to(`user:${userId}`).emit(event, data);
  }

  // Send to everyone in room
  toRoom(roomId: string, event: string, data: any): void {
    this.io.to(roomId).emit(event, data);
  }

  // Send to room except sender
  toRoomExcept(socket: Socket, roomId: string, event: string, data: any): void {
    socket.to(roomId).emit(event, data);
  }

  // Send to all connected clients
  toAll(event: string, data: any): void {
    this.io.emit(event, data);
  }

  // Send to multiple rooms
  toRooms(roomIds: string[], event: string, data: any): void {
    this.io.to(roomIds).emit(event, data);
  }

  // Send with acknowledgment
  async toUserWithAck(
    userId: string,
    event: string,
    data: any,
    timeout = 5000
  ): Promise<any> {
    const sockets = await this.io.in(`user:${userId}`).fetchSockets();

    if (sockets.length === 0) {
      throw new Error('User not connected');
    }

    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        reject(new Error('Acknowledgment timeout'));
      }, timeout);

      sockets[0].emit(event, data, (response: any) => {
        clearTimeout(timer);
        resolve(response);
      });
    });
  }
}

Message Queue Integration

// Using Redis as Socket.IO adapter
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';

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

await Promise.all([pubClient.connect(), subClient.connect()]);

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

// Cross-server broadcasting
// Server A
io.to('room:123').emit('message', { text: 'Hello' });

// Clients on Server B will also receive the message

// External service sending messages
async function sendFromExternalService(roomId: string, message: any) {
  await pubClient.publish('socket.io#/#', JSON.stringify({
    type: 2,
    nsp: '/',
    data: ['message', message],
    rooms: [roomId],
  }));
}

Real-Time Data Sync

// Collaborative editing example
interface Operation {
  type: 'insert' | 'delete';
  position: number;
  content?: string;
  length?: number;
  userId: string;
  timestamp: number;
}

class CollaborativeDocument {
  private content = '';
  private operations: Operation[] = [];
  private io: Server;
  private documentId: string;

  constructor(io: Server, documentId: string) {
    this.io = io;
    this.documentId = documentId;
    this.setupHandlers();
  }

  private setupHandlers(): void {
    this.io.on('connection', (socket) => {
      socket.on('doc:join', () => {
        socket.join(this.documentId);
        socket.emit('doc:sync', {
          content: this.content,
          version: this.operations.length,
        });
      });

      socket.on('doc:operation', (operation: Operation) => {
        this.applyOperation(operation);
        socket.to(this.documentId).emit('doc:operation', operation);
      });

      socket.on('doc:cursor', (position) => {
        socket.to(this.documentId).emit('doc:cursor', {
          userId: socket.data.user.id,
          position,
        });
      });
    });
  }

  private applyOperation(op: Operation): void {
    if (op.type === 'insert' && op.content) {
      this.content =
        this.content.slice(0, op.position) +
        op.content +
        this.content.slice(op.position);
    } else if (op.type === 'delete' && op.length) {
      this.content =
        this.content.slice(0, op.position) +
        this.content.slice(op.position + op.length);
    }

    this.operations.push(op);
  }
}

Performance Optimization

// Message compression
import { Server } from 'socket.io';

const io = new Server(httpServer, {
  perMessageDeflate: {
    threshold: 1024, // Compress above 1KB
    zlibDeflateOptions: {
      chunkSize: 16 * 1024,
    },
  },
});

// Message batching
class MessageBatcher {
  private batch: any[] = [];
  private timer: NodeJS.Timeout | null = null;
  private batchSize = 100;
  private flushInterval = 50;

  constructor(private socket: Socket) {}

  add(message: any): void {
    this.batch.push(message);

    if (this.batch.length >= this.batchSize) {
      this.flush();
    } else if (!this.timer) {
      this.timer = setTimeout(() => this.flush(), this.flushInterval);
    }
  }

  private flush(): void {
    if (this.batch.length > 0) {
      this.socket.emit('batch', this.batch);
      this.batch = [];
    }

    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = null;
    }
  }
}

Best Practices Summary

WebSocket Best Practices:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   Connection Management                             │
│   ├── Implement heartbeat detection               │
│   ├── Auto-reconnection mechanism                 │
│   ├── Connection state monitoring                 │
│   └── Graceful disconnect handling                │
│                                                     │
│   Message Design                                    │
│   ├── Use structured message format               │
│   ├── Implement message acknowledgment            │
│   ├── Consider message compression                │
│   └── Handle message ordering                     │
│                                                     │
│   Scalability                                       │
│   ├── Use Redis adapter                           │
│   ├── Rooms and namespaces                        │
│   ├── Load balancer configuration                 │
│   └── Connection count monitoring                 │
│                                                     │
│   Security                                          │
│   ├── Authentication                              │
│   ├── Message validation                          │
│   ├── Rate limiting                               │
│   └── Sensitive data encryption                   │
│                                                     │
└─────────────────────────────────────────────────────┘
ScenarioRecommended Solution
Chat appsSocket.IO
Game syncNative WebSocket
Data pushSSE
Collaborative editingSocket.IO + OT/CRDT
Notification systemSocket.IO

WebSocket brings true real-time capabilities to web apps. Choose the right solution and build smooth real-time experiences.


Real-time is user expectation, low latency is technical pursuit. Let data flow instantly, make interactions more natural.