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.