WebSocket Và Real-time — Chat, Notification, Live Data
Xây dựng ứng dụng real-time với WebSocket: từ kết nối cơ bản đến scale hàng nghìn client.
HTTP là request-response — client hỏi, server trả lời. WebSocket là kết nối 2 chiều, liên tục — cả hai đều có thể gửi bất kỳ lúc nào.
WebSocket vs HTTP#
| HTTP | WebSocket |
|---|---|
| Client gửi request → Server trả response | Kết nối persistent, 2 chiều |
| Mở/kết nối mỗi request | Handshake 1 lần, giữ kết nối |
| Polling cho real-time (kém hiệu quả) | Push real-time tự nhiên |
| Header lớn mỗi request | Header nhỏ sau handshake |
WebSocket Cơ Bản#
Server (Node.js + ws)#
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => {
console.log('Client connected');
// Nhận message từ client
ws.on('message', (data) => {
console.log('Received:', data.toString());
// Gửi lại cho client đó
ws.send(`Echo: ${data}`);
});
// Gửi message chào
ws.send('Welcome!');
ws.on('close', () => {
console.log('Client disconnected');
});
});typescriptClient#
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => {
console.log('Connected');
ws.send('Hello server!');
};
ws.onmessage = (event) => {
console.log('Server says:', event.data);
};
ws.onclose = () => {
console.log('Disconnected');
};
ws.onerror = (error) => {
console.error('Error:', error);
};typescriptBroadcast — Gửi Đến Tất Cả Client#
wss.on('connection', (ws) => {
ws.on('message', (data) => {
// Gửi cho tất cả client khác
wss.clients.forEach((client) => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(data.toString());
}
});
});
});typescriptRooms — Phòng Chat#
const rooms = new Map<string, Set<WebSocket>>();
function joinRoom(ws: WebSocket, room: string) {
if (!rooms.has(room)) {
rooms.set(room, new Set());
}
rooms.get(room)!.add(ws);
}
function broadcastToRoom(room: string, message: string, sender?: WebSocket) {
const clients = rooms.get(room);
if (!clients) return;
clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
// Usage
wss.on('connection', (ws) => {
ws.on('message', (data) => {
const { type, room, message } = JSON.parse(data.toString());
switch (type) {
case 'join':
joinRoom(ws, room);
break;
case 'message':
broadcastToRoom(room, message);
break;
}
});
});typescriptScale WebSocket — Nhiều Server#
Một server WebSocket không scale được — kết nối không share giữa các server.
Giải Pháp: Pub/Sub + Sticky Session#
Client A → Server 1 → Redis Pub/Sub → Server 2 → Client Bplaintextimport { createClient } from 'redis';
import { WebSocketServer } from 'ws';
const redis = createClient();
const pub = redis.duplicate();
const sub = redis.duplicate();
// Subscribe để nhận message từ server khác
sub.subscribe('chat:messages', (message) => {
// Broadcast đến client trên server này
wss.clients.forEach((client) => {
client.send(message);
});
});
wss.on('connection', (ws) => {
ws.on('message', (data) => {
// Publish lên Redis — các server khác cũng nhận được
pub.publish('chat:messages', data.toString());
// Broadcast local
// ...
});
});typescriptSticky session — đảm bảo client luôn kết nối cùng server (dùng IP hash cookie).
WebSocket với Bun#
Bun.serve({
port: 8080,
fetch(req, server) {
if (server.upgrade(req)) {
return; // WebSocket upgrade
}
return new Response('Hello');
},
websocket: {
open(ws) {
ws.subscribe('chat'); // Subscribe topic
},
message(ws, message) {
ws.publish('chat', message); // Broadcast trong topic
},
close(ws) {
ws.unsubscribe('chat');
},
},
});typescriptBun WebSocket built-in — không cần thư viện, pub/sub tích hợp sẵn.
WebSocket Với Cloudflare Durable Objects#
Durable Objects cho WebSocket real-time có state, scale global:
import { DurableObject } from 'cloudflare:workers';
export class ChatRoom extends DurableObject {
private sessions = new Map<string, WebSocket>();
async fetch(request: Request) {
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
this.sessions.set(this.ctx.id.toString(), server);
server.accept();
server.addEventListener('message', (event) => {
this.sessions.forEach((ws) => {
ws.send(event.data);
});
});
return new Response(null, { status: 101, webSocket: client });
}
}typescriptAuth WebSocket#
Xác thực khi handshake:
wss.on('connection', (ws, request) => {
const token = new URL(request.url!, `http://${request.headers.host}`)
.searchParams.get('token');
if (!token || !verifyToken(token)) {
ws.close(4001, 'Unauthorized');
return;
}
const user = decodeToken(token);
ws.userId = user.id;
});typescriptClient:
const ws = new WebSocket('ws://localhost:8080?token=abc123');typescriptReconnection — Auto Retry#
class ReconnectingWebSocket {
private ws: WebSocket | null = null;
private url: string;
private retries = 0;
private maxRetries = 10;
constructor(url: string) {
this.url = url;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onclose = () => {
if (this.retries < this.maxRetries) {
const delay = Math.min(1000 * 2 ** this.retries, 30000);
setTimeout(() => {
this.retries++;
this.connect();
}, delay);
}
};
this.ws.onopen = () => {
this.retries = 0;
};
}
send(data: string) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(data);
}
}
}typescriptKhi Nào Dùng WebSocket?#
- Chat, messaging
- Live notifications
- Collaborative editing (Google Docs)
- Real-time dashboard, trading
- Multiplayer games
- Live streaming updates
Không dùng WebSocket cho: CRUD API, request-response đơn giản — HTTP vẫn tốt hơn.
Kết Luận#
WebSocket là công cụ mạnh cho real-time. Bắt đầu đơn giản với ws (Node.js) hoặc built-in WebSocket của Bun. Scale với Redis Pub/Sub. Nhớ xử lý reconnection, auth, và heartbeat (ping/pong) để kết nối ổn định.