Building Real-time Applications with WebSockets: A Comprehensive Guide


Building Real-time Applications with WebSockets: A Comprehensive Guide

Real-time applications are becoming increasingly important in modern web development. WebSockets provide a powerful way to implement real-time features. This guide will show you how to build various types of real-time applications.

Understanding WebSockets

WebSockets provide full-duplex communication channels over a single TCP connection:

// Browser-side WebSocket
const ws = new WebSocket('ws://localhost:8080')

ws.onopen = () => {
  console.log('Connected to WebSocket server')
}

ws.onmessage = (event) => {
  console.log('Received:', event.data)
}

ws.onclose = () => {
  console.log('Disconnected from WebSocket server')
}

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

Setting Up a WebSocket Server

Let's create a WebSocket server using Node.js and ws library:

// server.js
const WebSocket = require('ws')
const server = new WebSocket.Server({ port: 8080 })

server.on('connection', (ws) => {
  console.log('New client connected')
  
  ws.on('message', (message) => {
    console.log('Received:', message)
    
    // Broadcast to all clients
    server.clients.forEach((client) => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(message)
      }
    })
  })
  
  ws.on('close', () => {
    console.log('Client disconnected')
  })
})

Project: Real-time Chat Application

Let's build a complete chat application with rooms and private messaging:

// types.ts
interface ChatMessage {
  id: string
  type: 'message' | 'system'
  room: string
  sender: string
  content: string
  timestamp: number
}

interface ChatRoom {
  id: string
  name: string
  users: Set<string>
}

// server/ChatServer.ts
import { WebSocket, WebSocketServer } from 'ws'
import { v4 as uuidv4 } from 'uuid'

class ChatServer {
  private wss: WebSocketServer
  private rooms: Map<string, ChatRoom>
  private clients: Map<WebSocket, string> // WebSocket -> userId
  
  constructor(port: number) {
    this.wss = new WebSocketServer({ port })
    this.rooms = new Map()
    this.clients = new Map()
    
    // Create default room
    this.rooms.set('general', {
      id: 'general',
      name: 'General',
      users: new Set()
    })
    
    this.setupWebSocketServer()
  }
  
  private setupWebSocketServer() {
    this.wss.on('connection', (ws: WebSocket) => {
      const userId = uuidv4()
      this.clients.set(ws, userId)
      
      // Join default room
      this.joinRoom(ws, 'general')
      
      ws.on('message', (data: string) => {
        const message = JSON.parse(data)
        this.handleMessage(ws, message)
      })
      
      ws.on('close', () => {
        this.handleDisconnect(ws)
      })
    })
  }
  
  private handleMessage(ws: WebSocket, message: any) {
    const userId = this.clients.get(ws)
    
    switch (message.type) {
      case 'chat':
        this.broadcastToRoom(message.room, {
          id: uuidv4(),
          type: 'message',
          room: message.room,
          sender: userId,
          content: message.content,
          timestamp: Date.now()
        })
        break
        
      case 'join_room':
        this.joinRoom(ws, message.room)
        break
        
      case 'leave_room':
        this.leaveRoom(ws, message.room)
        break
        
      case 'private_message':
        this.sendPrivateMessage(
          userId!,
          message.recipient,
          message.content
        )
        break
    }
  }
  
  private broadcastToRoom(roomId: string, message: ChatMessage) {
    const room = this.rooms.get(roomId)
    if (!room) return
    
    this.wss.clients.forEach((client) => {
      if (
        client.readyState === WebSocket.OPEN &&
        room.users.has(this.clients.get(client)!)
      ) {
        client.send(JSON.stringify(message))
      }
    })
  }
  
  private joinRoom(ws: WebSocket, roomId: string) {
    const userId = this.clients.get(ws)
    const room = this.rooms.get(roomId)
    
    if (!room || !userId) return
    
    room.users.add(userId)
    
    // Notify room about new user
    this.broadcastToRoom(roomId, {
      id: uuidv4(),
      type: 'system',
      room: roomId,
      sender: 'system',
      content: `User ${userId} joined the room`,
      timestamp: Date.now()
    })
  }
  
  private leaveRoom(ws: WebSocket, roomId: string) {
    const userId = this.clients.get(ws)
    const room = this.rooms.get(roomId)
    
    if (!room || !userId) return
    
    room.users.delete(userId)
    
    // Notify room about user leaving
    this.broadcastToRoom(roomId, {
      id: uuidv4(),
      type: 'system',
      room: roomId,
      sender: 'system',
      content: `User ${userId} left the room`,
      timestamp: Date.now()
    })
  }
  
  private sendPrivateMessage(
    senderId: string,
    recipientId: string,
    content: string
  ) {
    const message = {
      id: uuidv4(),
      type: 'private_message',
      sender: senderId,
      content,
      timestamp: Date.now()
    }
    
    this.wss.clients.forEach((client) => {
      const clientId = this.clients.get(client)
      if (
        client.readyState === WebSocket.OPEN &&
        clientId === recipientId
      ) {
        client.send(JSON.stringify(message))
      }
    })
  }
  
  private handleDisconnect(ws: WebSocket) {
    const userId = this.clients.get(ws)
    if (!userId) return
    
    // Remove user from all rooms
    this.rooms.forEach((room) => {
      if (room.users.has(userId)) {
        room.users.delete(userId)
        this.broadcastToRoom(room.id, {
          id: uuidv4(),
          type: 'system',
          room: room.id,
          sender: 'system',
          content: `User ${userId} disconnected`,
          timestamp: Date.now()
        })
      }
    })
    
    this.clients.delete(ws)
  }
}

// client/ChatClient.ts
class ChatClient {
  private ws: WebSocket
  private messageHandlers: Map<string, (message: any) => void>
  
  constructor(url: string) {
    this.ws = new WebSocket(url)
    this.messageHandlers = new Map()
    
    this.ws.onmessage = (event) => {
      const message = JSON.parse(event.data)
      this.handleMessage(message)
    }
  }
  
  public sendMessage(room: string, content: string) {
    this.ws.send(JSON.stringify({
      type: 'chat',
      room,
      content
    }))
  }
  
  public joinRoom(room: string) {
    this.ws.send(JSON.stringify({
      type: 'join_room',
      room
    }))
  }
  
  public leaveRoom(room: string) {
    this.ws.send(JSON.stringify({
      type: 'leave_room',
      room
    }))
  }
  
  public sendPrivateMessage(recipient: string, content: string) {
    this.ws.send(JSON.stringify({
      type: 'private_message',
      recipient,
      content
    }))
  }
  
  public onMessage(type: string, handler: (message: any) => void) {
    this.messageHandlers.set(type, handler)
  }
  
  private handleMessage(message: any) {
    const handler = this.messageHandlers.get(message.type)
    if (handler) {
      handler(message)
    }
  }
}

// React component example
import React, { useEffect, useState } from 'react'

function ChatRoom({ roomId }: { roomId: string }) {
  const [messages, setMessages] = useState<ChatMessage[]>([])
  const [input, setInput] = useState('')
  const [client, setClient] = useState<ChatClient | null>(null)
  
  useEffect(() => {
    const chatClient = new ChatClient('ws://localhost:8080')
    setClient(chatClient)
    
    chatClient.onMessage('message', (message) => {
      setMessages((prev) => [...prev, message])
    })
    
    chatClient.onMessage('system', (message) => {
      setMessages((prev) => [...prev, message])
    })
    
    chatClient.joinRoom(roomId)
    
    return () => {
      chatClient.leaveRoom(roomId)
    }
  }, [roomId])
  
  const sendMessage = () => {
    if (input.trim() && client) {
      client.sendMessage(roomId, input)
      setInput('')
    }
  }
  
  return (
    <div className="chat-room">
      <div className="messages">
        {messages.map((message) => (
          <div
            key={message.id}
            className={`message ${message.type}`}
          >
            <span className="sender">{message.sender}</span>
            <span className="content">{message.content}</span>
            <span className="time">
              {new Date(message.timestamp).toLocaleTimeString()}
            </span>
          </div>
        ))}
      </div>
      
      <div className="input-area">
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
        />
        <button onClick={sendMessage}>Send</button>
      </div>
    </div>
  )
}

Real-time Dashboard Example

Let's create a real-time dashboard that updates automatically:

// server/DashboardServer.ts
class DashboardServer {
  private wss: WebSocketServer
  private metrics: Map<string, number>
  private updateInterval: NodeJS.Timer
  
  constructor(port: number) {
    this.wss = new WebSocketServer({ port })
    this.metrics = new Map()
    
    this.setupWebSocketServer()
    this.startMetricsUpdate()
  }
  
  private setupWebSocketServer() {
    this.wss.on('connection', (ws: WebSocket) => {
      // Send initial metrics
      ws.send(JSON.stringify({
        type: 'metrics',
        data: Object.fromEntries(this.metrics)
      }))
    })
  }
  
  private startMetricsUpdate() {
    this.updateInterval = setInterval(() => {
      // Update metrics
      this.metrics.set('cpu', Math.random() * 100)
      this.metrics.set('memory', Math.random() * 16384)
      this.metrics.set('requests', Math.floor(Math.random() * 1000))
      
      // Broadcast to all clients
      this.broadcastMetrics()
    }, 1000)
  }
  
  private broadcastMetrics() {
    const message = JSON.stringify({
      type: 'metrics',
      data: Object.fromEntries(this.metrics)
    })
    
    this.wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(message)
      }
    })
  }
}

// React dashboard component
function Dashboard() {
  const [metrics, setMetrics] = useState({
    cpu: 0,
    memory: 0,
    requests: 0
  })
  
  useEffect(() => {
    const ws = new WebSocket('ws://localhost:8080')
    
    ws.onmessage = (event) => {
      const message = JSON.parse(event.data)
      if (message.type === 'metrics') {
        setMetrics(message.data)
      }
    }
    
    return () => ws.close()
  }, [])
  
  return (
    <div className="dashboard">
      <div className="metric">
        <h3>CPU Usage</h3>
        <div className="value">{metrics.cpu.toFixed(1)}%</div>
      </div>
      
      <div className="metric">
        <h3>Memory Usage</h3>
        <div className="value">
          {(metrics.memory / 1024).toFixed(2)} GB
        </div>
      </div>
      
      <div className="metric">
        <h3>Requests/sec</h3>
        <div className="value">{metrics.requests}</div>
      </div>
    </div>
  )
}

Best Practices

  1. Connection Management

    • Implement reconnection logic
    • Handle connection errors
    • Clean up resources properly
    • Monitor connection health
  2. Performance

    • Minimize message size
    • Batch updates when possible
    • Use binary protocols for large data
    • Implement rate limiting
  3. Security

    • Use WSS (WebSocket Secure)
    • Validate messages
    • Implement authentication
    • Prevent DoS attacks
  4. Scalability

    • Use Redis for pub/sub
    • Implement horizontal scaling
    • Monitor performance
    • Handle backpressure

Common WebSocket Use Cases

  1. Real-time Collaboration
interface CursorPosition {
  userId: string
  x: number
  y: number
}

// Broadcast cursor positions
ws.send(JSON.stringify({
  type: 'cursor_move',
  position: { x, y }
}))
  1. Live Updates
// Subscribe to updates
ws.send(JSON.stringify({
  type: 'subscribe',
  topics: ['prices', 'inventory']
}))
  1. Multiplayer Games
interface GameState {
  players: Map<string, PlayerState>
  gameObjects: GameObject[]
}

// Send player action
ws.send(JSON.stringify({
  type: 'player_action',
  action: 'move',
  direction: { x: 1, y: 0 }
}))

Conclusion

WebSockets enable powerful real-time features in web applications:

  • Bi-directional communication
  • Low latency updates
  • Efficient resource usage
  • Scalable architecture

Keep exploring different use cases and implementing best practices for robust real-time applications.


Further Reading