WebSocket Real-Time Communication: Building Interactive Apps

WebSocket Real-Time Communication: Building Interactive Apps

WebSockets enable full-duplex communication between client and server, perfect for real-time features like chat, notifications, and live updates. Let’s build practical real-time applications.

Basic WebSocket Server (Node.js)

Create a simple WebSocket server using the ‘ws’ library:

const WebSocket = require("ws");
const http = require("http");

// Create HTTP server
const server = http.createServer();

// Create WebSocket server
const wss = new WebSocket.Server({ server });

// Connection handler
wss.on("connection", (ws, req) => {
  console.log("New client connected");

  // Send welcome message
  ws.send(
    JSON.stringify({
      type: "welcome",
      message: "Connected to WebSocket server",
    }),
  );

  // Message handler
  ws.on("message", (data) => {
    console.log("Received:", data.toString());

    try {
      const message = JSON.parse(data);

      // Echo message back to client
      ws.send(
        JSON.stringify({
          type: "echo",
          data: message,
        }),
      );
    } catch (error) {
      ws.send(
        JSON.stringify({
          type: "error",
          message: "Invalid message format",
        }),
      );
    }
  });

  // Error handler
  ws.on("error", (error) => {
    console.error("WebSocket error:", error);
  });

  // Close handler
  ws.on("close", () => {
    console.log("Client disconnected");
  });
});

// Start server
const PORT = 8080;
server.listen(PORT, () => {
  console.log(`WebSocket server running on port ${PORT}`);
});

Basic WebSocket Client

Connect from the browser:

class WebSocketClient {
  constructor(url) {
    this.url = url;
    this.ws = null;
    this.reconnectInterval = 3000;
    this.messageHandlers = new Map();
  }

  connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      console.log("Connected to server");
      this.onOpen();
    };

    this.ws.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data);
        this.handleMessage(data);
      } catch (error) {
        console.error("Failed to parse message:", error);
      }
    };

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

    this.ws.onclose = () => {
      console.log("Disconnected from server");
      this.reconnect();
    };
  }

  reconnect() {
    setTimeout(() => {
      console.log("Attempting to reconnect...");
      this.connect();
    }, this.reconnectInterval);
  }

  send(type, data) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({ type, data }));
    } else {
      console.error("WebSocket is not connected");
    }
  }

  on(type, handler) {
    this.messageHandlers.set(type, handler);
  }

  handleMessage(message) {
    const handler = this.messageHandlers.get(message.type);
    if (handler) {
      handler(message.data);
    }
  }

  onOpen() {
    // Override in subclass
  }

  disconnect() {
    if (this.ws) {
      this.ws.close();
    }
  }
}

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

client.on("welcome", (data) => {
  console.log("Welcome message:", data);
});

client.on("echo", (data) => {
  console.log("Echo received:", data);
});

// Send message
setTimeout(() => {
  client.send("chat", { message: "Hello, server!" });
}, 1000);

Real-Time Chat Application

Complete chat server with rooms and user management:

const WebSocket = require("ws");
const http = require("http");
const { v4: uuidv4 } = require("uuid");

class ChatServer {
  constructor(port) {
    this.port = port;
    this.clients = new Map();
    this.rooms = new Map();
    this.init();
  }

  init() {
    const server = http.createServer();
    this.wss = new WebSocket.Server({ server });

    this.wss.on("connection", (ws, req) => {
      this.handleConnection(ws, req);
    });

    server.listen(this.port, () => {
      console.log(`Chat server running on port ${this.port}`);
    });
  }

  handleConnection(ws, req) {
    const clientId = uuidv4();
    const client = {
      id: clientId,
      ws: ws,
      username: null,
      room: null,
    };

    this.clients.set(clientId, client);

    ws.on("message", (data) => {
      this.handleMessage(clientId, data);
    });

    ws.on("close", () => {
      this.handleDisconnect(clientId);
    });

    this.sendToClient(clientId, {
      type: "connected",
      clientId: clientId,
    });
  }

  handleMessage(clientId, data) {
    try {
      const message = JSON.parse(data);
      const client = this.clients.get(clientId);

      switch (message.type) {
        case "setUsername":
          this.setUsername(clientId, message.username);
          break;

        case "joinRoom":
          this.joinRoom(clientId, message.room);
          break;

        case "leaveRoom":
          this.leaveRoom(clientId);
          break;

        case "chat":
          this.broadcastMessage(clientId, message.message);
          break;

        case "privateMessage":
          this.sendPrivateMessage(clientId, message.to, message.message);
          break;

        default:
          console.log("Unknown message type:", message.type);
      }
    } catch (error) {
      console.error("Error handling message:", error);
    }
  }

  setUsername(clientId, username) {
    const client = this.clients.get(clientId);
    client.username = username;

    this.sendToClient(clientId, {
      type: "usernameSet",
      username: username,
    });
  }

  joinRoom(clientId, roomName) {
    const client = this.clients.get(clientId);

    // Leave current room if in one
    if (client.room) {
      this.leaveRoom(clientId);
    }

    // Create room if doesn't exist
    if (!this.rooms.has(roomName)) {
      this.rooms.set(roomName, new Set());
    }

    // Add client to room
    this.rooms.get(roomName).add(clientId);
    client.room = roomName;

    // Notify client
    this.sendToClient(clientId, {
      type: "roomJoined",
      room: roomName,
      users: this.getRoomUsers(roomName),
    });

    // Notify others in room
    this.broadcastToRoom(
      roomName,
      {
        type: "userJoined",
        username: client.username,
        userId: clientId,
      },
      clientId,
    );
  }

  leaveRoom(clientId) {
    const client = this.clients.get(clientId);
    if (!client.room) return;

    const room = this.rooms.get(client.room);
    room.delete(clientId);

    // Notify others in room
    this.broadcastToRoom(
      client.room,
      {
        type: "userLeft",
        username: client.username,
        userId: clientId,
      },
      clientId,
    );

    // Clean up empty room
    if (room.size === 0) {
      this.rooms.delete(client.room);
    }

    client.room = null;
  }

  broadcastMessage(clientId, message) {
    const client = this.clients.get(clientId);
    if (!client.room) return;

    this.broadcastToRoom(client.room, {
      type: "chat",
      username: client.username,
      userId: clientId,
      message: message,
      timestamp: new Date().toISOString(),
    });
  }

  sendPrivateMessage(fromId, toId, message) {
    const fromClient = this.clients.get(fromId);
    const toClient = this.clients.get(toId);

    if (!toClient) {
      this.sendToClient(fromId, {
        type: "error",
        message: "User not found",
      });
      return;
    }

    const messageData = {
      type: "privateMessage",
      from: fromClient.username,
      fromId: fromId,
      message: message,
      timestamp: new Date().toISOString(),
    };

    this.sendToClient(toId, messageData);
    this.sendToClient(fromId, { ...messageData, type: "privateMessageSent" });
  }

  broadcastToRoom(roomName, message, excludeId = null) {
    const room = this.rooms.get(roomName);
    if (!room) return;

    room.forEach((clientId) => {
      if (clientId !== excludeId) {
        this.sendToClient(clientId, message);
      }
    });
  }

  sendToClient(clientId, message) {
    const client = this.clients.get(clientId);
    if (client && client.ws.readyState === WebSocket.OPEN) {
      client.ws.send(JSON.stringify(message));
    }
  }

  getRoomUsers(roomName) {
    const room = this.rooms.get(roomName);
    if (!room) return [];

    return Array.from(room).map((clientId) => {
      const client = this.clients.get(clientId);
      return {
        id: client.id,
        username: client.username,
      };
    });
  }

  handleDisconnect(clientId) {
    const client = this.clients.get(clientId);
    if (client.room) {
      this.leaveRoom(clientId);
    }
    this.clients.delete(clientId);
    console.log(`Client ${clientId} disconnected`);
  }
}

// Start chat server
const chatServer = new ChatServer(8080);

Chat Client Implementation

React chat client:

import React, { useState, useEffect, useRef } from "react";

function ChatApp() {
  const [ws, setWs] = useState(null);
  const [connected, setConnected] = useState(false);
  const [username, setUsername] = useState("");
  const [room, setRoom] = useState("");
  const [message, setMessage] = useState("");
  const [messages, setMessages] = useState([]);
  const [users, setUsers] = useState([]);

  useEffect(() => {
    const socket = new WebSocket("ws://localhost:8080");

    socket.onopen = () => {
      setConnected(true);
      console.log("Connected to chat server");
    };

    socket.onmessage = (event) => {
      const data = JSON.parse(event.data);
      handleMessage(data);
    };

    socket.onclose = () => {
      setConnected(false);
      console.log("Disconnected from server");
    };

    setWs(socket);

    return () => {
      socket.close();
    };
  }, []);

  const handleMessage = (data) => {
    switch (data.type) {
      case "roomJoined":
        setUsers(data.users);
        break;

      case "userJoined":
        setUsers((prev) => [
          ...prev,
          { id: data.userId, username: data.username },
        ]);
        addSystemMessage(`${data.username} joined the room`);
        break;

      case "userLeft":
        setUsers((prev) => prev.filter((u) => u.id !== data.userId));
        addSystemMessage(`${data.username} left the room`);
        break;

      case "chat":
        setMessages((prev) => [
          ...prev,
          {
            id: Date.now(),
            username: data.username,
            message: data.message,
            timestamp: data.timestamp,
            isOwn: false,
          },
        ]);
        break;

      default:
        console.log("Unhandled message:", data);
    }
  };

  const addSystemMessage = (text) => {
    setMessages((prev) => [
      ...prev,
      {
        id: Date.now(),
        isSystem: true,
        message: text,
      },
    ]);
  };

  const setUserUsername = () => {
    if (ws && username.trim()) {
      ws.send(
        JSON.stringify({
          type: "setUsername",
          username: username.trim(),
        }),
      );
    }
  };

  const joinChatRoom = () => {
    if (ws && room.trim()) {
      ws.send(
        JSON.stringify({
          type: "joinRoom",
          room: room.trim(),
        }),
      );
    }
  };

  const sendMessage = () => {
    if (ws && message.trim()) {
      ws.send(
        JSON.stringify({
          type: "chat",
          message: message.trim(),
        }),
      );

      setMessages((prev) => [
        ...prev,
        {
          id: Date.now(),
          username: "You",
          message: message.trim(),
          timestamp: new Date().toISOString(),
          isOwn: true,
        },
      ]);

      setMessage("");
    }
  };

  return (
    <div className="chat-app">
      <div className="status">
        {connected ? "🟢 Connected" : "🔴 Disconnected"}
      </div>

      <div className="setup">
        <input
          value={username}
          onChange={(e) => setUsername(e.target.value)}
          placeholder="Enter username"
        />
        <button onClick={setUserUsername}>Set Username</button>

        <input
          value={room}
          onChange={(e) => setRoom(e.target.value)}
          placeholder="Enter room name"
        />
        <button onClick={joinChatRoom}>Join Room</button>
      </div>

      <div className="chat-container">
        <div className="users">
          <h3>Users ({users.length})</h3>
          {users.map((user) => (
            <div key={user.id}>{user.username}</div>
          ))}
        </div>

        <div className="messages">
          {messages.map((msg) => (
            <div
              key={msg.id}
              className={`message ${msg.isOwn ? "own" : ""} ${msg.isSystem ? "system" : ""}`}
            >
              {!msg.isSystem && <strong>{msg.username}: </strong>}
              {msg.message}
            </div>
          ))}
        </div>
      </div>

      <div className="input-area">
        <input
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          onKeyPress={(e) => e.key === "Enter" && sendMessage()}
          placeholder="Type a message..."
        />
        <button onClick={sendMessage}>Send</button>
      </div>
    </div>
  );
}

export default ChatApp;

Socket.IO Alternative

Using Socket.IO for easier WebSocket handling:

// Server
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: "*" },
});

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

  socket.on("join-room", (room) => {
    socket.join(room);
    socket.to(room).emit("user-joined", socket.id);
  });

  socket.on("send-message", (room, message) => {
    socket.to(room).emit("receive-message", {
      senderId: socket.id,
      message: message,
    });
  });

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

server.listen(3000, () => {
  console.log("Server running on port 3000");
});

// Client
import io from "socket.io-client";

const socket = io("http://localhost:3000");

socket.on("connect", () => {
  console.log("Connected");
  socket.emit("join-room", "room1");
});

socket.on("receive-message", (data) => {
  console.log("Message:", data.message);
});

socket.emit("send-message", "room1", "Hello!");

Conclusion

WebSockets enable powerful real-time features in modern web applications. Whether using raw WebSockets or libraries like Socket.IO, understanding the fundamentals helps you build responsive, interactive experiences. Start simple and scale up as your real-time needs grow.