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
-
Connection Management
- Implement reconnection logic
- Handle connection errors
- Clean up resources properly
- Monitor connection health
-
Performance
- Minimize message size
- Batch updates when possible
- Use binary protocols for large data
- Implement rate limiting
-
Security
- Use WSS (WebSocket Secure)
- Validate messages
- Implement authentication
- Prevent DoS attacks
-
Scalability
- Use Redis for pub/sub
- Implement horizontal scaling
- Monitor performance
- Handle backpressure
Common WebSocket Use Cases
- Real-time Collaboration
interface CursorPosition {
userId: string
x: number
y: number
}
// Broadcast cursor positions
ws.send(JSON.stringify({
type: 'cursor_move',
position: { x, y }
}))
- Live Updates
// Subscribe to updates
ws.send(JSON.stringify({
type: 'subscribe',
topics: ['prices', 'inventory']
}))
- 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.