1. Polling là gì?
Polling là kỹ thuật client định kỳ gửi request đến server để kiểm tra dữ liệu mới. Có hai loại polling chính:
1.1 Short Polling
async function shortPolling() {
while (true) {
try {
const response = await fetch("/api/updates");
const data = await response.json();
processData(data);
} catch (error) {
console.error("Polling error:", error);
}
// Đợi 5 giây trước khi gửi request tiếp theo
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
function processData(data) {
console.log("New data:", data);
}
1.2 Long Polling
interface UpdateResponse {
data: any;
timestamp: number;
}
class LongPollingClient {
private lastTimestamp: number = 0;
private endpoint: string;
private timeout: number;
constructor(endpoint: string, timeout: number = 30000) {
this.endpoint = endpoint;
this.timeout = timeout;
}
async startPolling() {
while (true) {
try {
const response = await fetch(
`${this.endpoint}?lastTimestamp=${this.lastTimestamp}`,
{
signal: AbortSignal.timeout(this.timeout),
}
);
if (response.ok) {
const data: UpdateResponse = await response.json();
this.lastTimestamp = data.timestamp;
this.handleUpdate(data);
}
} catch (error) {
if (error instanceof DOMException && error.name === "TimeoutError") {
continue; // Restart polling on timeout
}
console.error("Long polling error:", error);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
}
private handleUpdate(data: UpdateResponse) {
console.log("Received update:", data);
}
}
// Usage
const client = new LongPollingClient("/api/updates");
client.startPolling();
2. WebSockets là gì?
WebSocket là giao thức cho phép giao tiếp hai chiều giữa client và server qua một kết nối duy nhất.
2.1 Basic WebSocket Implementation
class WebSocketClient {
private ws: WebSocket;
private reconnectAttempts: number = 0;
private maxReconnectAttempts: number = 5;
private reconnectDelay: number = 1000;
constructor(private url: string) {
this.connect();
}
private connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log("Connected to server");
this.reconnectAttempts = 0;
};
this.ws.onmessage = (event) => {
this.handleMessage(event.data);
};
this.ws.onclose = () => {
console.log("Connection closed");
this.handleReconnect();
};
this.ws.onerror = (error) => {
console.error("WebSocket error:", error);
};
}
private handleReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`Reconnecting... Attempt ${this.reconnectAttempts}`);
setTimeout(() => {
this.connect();
}, this.reconnectDelay * this.reconnectAttempts);
} else {
console.error("Max reconnection attempts reached");
}
}
private handleMessage(data: string) {
try {
const parsedData = JSON.parse(data);
console.log("Received:", parsedData);
} catch (error) {
console.error("Error parsing message:", error);
}
}
public send(data: any) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
} else {
console.error("Connection not open");
}
}
public close() {
this.ws.close();
}
}
2.2 Type-safe WebSocket với TypeScript
interface WebSocketMessage<T = any> {
type: string;
payload: T;
}
interface ChatMessage {
id: string;
user: string;
content: string;
timestamp: number;
}
class TypedWebSocketClient {
private ws: WebSocket;
private messageHandlers: Map<string, (payload: any) => void>;
constructor(url: string) {
this.ws = new WebSocket(url);
this.messageHandlers = new Map();
this.setupWebSocket();
}
private setupWebSocket() {
this.ws.onmessage = (event) => {
try {
const message: WebSocketMessage = JSON.parse(event.data);
const handler = this.messageHandlers.get(message.type);
if (handler) {
handler(message.payload);
}
} catch (error) {
console.error("Error handling message:", error);
}
};
}
public on<T>(type: string, handler: (payload: T) => void) {
this.messageHandlers.set(type, handler);
}
public send<T>(type: string, payload: T) {
const message: WebSocketMessage<T> = { type, payload };
this.ws.send(JSON.stringify(message));
}
}
// Usage
const chat = new TypedWebSocketClient("ws://chat.server");
chat.on<ChatMessage>("message", (message) => {
console.log(`${message.user}: ${message.content}`);
});
chat.send("message", {
id: crypto.randomUUID(),
user: "John",
content: "Hello!",
timestamp: Date.now(),
});
3. Ví dụ Thực Tế: Real-time Chat Application
3.1 Polling Version
interface ChatState {
messages: ChatMessage[];
users: string[];
lastUpdate: number;
}
class PollingChatClient {
private state: ChatState = {
messages: [],
users: [],
lastUpdate: 0,
};
constructor(private apiUrl: string) {}
async start() {
this.pollMessages();
this.pollUsers();
}
private async pollMessages() {
while (true) {
try {
const response = await fetch(
`${this.apiUrl}/messages?since=${this.state.lastUpdate}`
);
const data = await response.json();
if (data.messages.length > 0) {
this.state.messages.push(...data.messages);
this.state.lastUpdate = data.timestamp;
this.renderMessages();
}
} catch (error) {
console.error("Error polling messages:", error);
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
private async pollUsers() {
while (true) {
try {
const response = await fetch(`${this.apiUrl}/users`);
const users = await response.json();
if (JSON.stringify(users) !== JSON.stringify(this.state.users)) {
this.state.users = users;
this.renderUsers();
}
} catch (error) {
console.error("Error polling users:", error);
}
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
private renderMessages() {
// Update UI with new messages
}
private renderUsers() {
// Update UI with user list
}
}
3.2 WebSocket Version
interface ChatEvent {
type: "message" | "user_joined" | "user_left";
payload: any;
}
class WebSocketChatClient {
private ws: WebSocket;
private state: ChatState = {
messages: [],
users: [],
lastUpdate: 0,
};
constructor(private wsUrl: string) {
this.connect();
}
private connect() {
this.ws = new WebSocket(this.wsUrl);
this.ws.onmessage = (event) => {
const chatEvent: ChatEvent = JSON.parse(event.data);
switch (chatEvent.type) {
case "message":
this.handleNewMessage(chatEvent.payload);
break;
case "user_joined":
this.handleUserJoined(chatEvent.payload);
break;
case "user_left":
this.handleUserLeft(chatEvent.payload);
break;
}
};
this.ws.onclose = () => {
console.log("Connection lost, reconnecting...");
setTimeout(() => this.connect(), 1000);
};
}
private handleNewMessage(message: ChatMessage) {
this.state.messages.push(message);
this.state.lastUpdate = Date.now();
this.renderMessages();
}
private handleUserJoined(user: string) {
if (!this.state.users.includes(user)) {
this.state.users.push(user);
this.renderUsers();
}
}
private handleUserLeft(user: string) {
const index = this.state.users.indexOf(user);
if (index !== -1) {
this.state.users.splice(index, 1);
this.renderUsers();
}
}
public sendMessage(content: string) {
const message: ChatMessage = {
id: crypto.randomUUID(),
user: "current_user",
content,
timestamp: Date.now(),
};
this.ws.send(
JSON.stringify({
type: "message",
payload: message,
})
);
}
private renderMessages() {
// Update UI with new messages
}
private renderUsers() {
// Update UI with user list
}
}
3.5 Server-Sent Events (SSE), nửa đường giữa Polling và WebSocket
Giữa “dội liên tục” và “kết nối full-duplex”, SSE (EventSource) là lựa chọn thường bị bỏ quên: server đẩy một chiều xuống client qua một HTTP response mở kéo dài. Ưu thế:
- Chạy trên HTTP/1.1, HTTP/2, HTTP/3 nguyên thuỷ, qua được mọi proxy, CDN, load balancer.
- Auto-reconnect và Last-Event-ID resume built-in trong browser.
- Không cần thay đổi giao thức (không upgrade như WebSocket).
- Gọn hơn WebSocket khi chỉ cần server → client (notification, log tail, AI streaming token, progress bar).
// Client
const source = new EventSource("/api/stream", { withCredentials: true });
source.addEventListener("message", (e) => {
const data = JSON.parse(e.data);
console.log("Event:", data);
});
source.addEventListener("ping", (e) => console.log("heartbeat"));
source.onerror = () => {
// Browser tự reconnect, chỉ cần đóng khi muốn stop hẳn
console.warn("SSE disconnected, browser will retry");
};
// Huỷ khi rời trang
source.close();
// Server (Node.js / Express)
app.get("/api/stream", (req, res) => {
res.set({
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
"X-Accel-Buffering": "no", // bắt nginx không buffer
});
res.flushHeaders();
let id = 0;
const timer = setInterval(() => {
res.write(`id: ${++id}\n`);
res.write(`event: message\n`);
res.write(`data: ${JSON.stringify({ time: Date.now() })}\n\n`);
}, 1000);
req.on("close", () => clearInterval(timer));
});
Chú ý giới hạn của SSE:
- Chỉ một chiều (client muốn gửi → dùng
fetchPOST riêng). - HTTP/1.1 có giới hạn 6 kết nối đồng thời per domain → SSE mở liên tục dễ “ăn slot”. Với HTTP/2/3 thì không (multiplexing).
3.6 Bảng so sánh 4 kỹ thuật thường dùng cho “real-time”
| Tiêu chí | Short polling | Long polling | SSE (EventSource) | WebSocket |
|---|---|---|---|---|
| Chiều | Client pull | Client pull | Server → Client | Full duplex |
| Giao thức | HTTP request bình thường | HTTP request dài | HTTP streaming | Upgrade → ws/wss |
| Proxy / CDN friendly | Tốt | Vừa | Tốt | Phải cấu hình |
| Auto-reconnect | Tự dùng setInterval | Tự code | Built-in browser | Tự code (hoặc lib) |
| Độ trễ | Cao (= chu kỳ poll) | Trung bình | Thấp | Rất thấp |
| Server cost (N clients) | Cao nhất | Trung bình | Một kết nối mở | Một kết nối mở |
| Binary | Không | Không | Không (text/UTF-8) | Có |
| Client → Server | Tách request | Tách request | Tách request | Cùng kênh |
| Use case phù hợp | Dashboard thỉnh thoảng | Notification nhẹ | AI token streaming, progress, log tail, notification | Chat, multiplayer, collab editor, WebRTC signaling |
103 Early Hints (RFC 8297) đôi khi bị nhầm với “server push”, nó không phải stream data; đó là HTTP 1xx informational để gợi ý
<link rel="preload">/<link rel="preconnect">trong khi server vẫn đang compute response chính. Hữu ích cho page load performance, không phải real-time.
3.7 Heuristic chọn nhanh
- Server chỉ đẩy, client ít gửi (dashboard, monitor, AI stream, notification) → SSE.
- Hai chiều thường xuyên (chat, game, collab) → WebSocket.
- Dữ liệu thay đổi chậm (>= 30s) hoặc infra đơn giản → Short polling + cache headers.
- Client cần cập nhật gần real-time nhưng hạ tầng không cho WebSocket (corporate proxy, legacy LB) → Long polling hoặc SSE.
- Tải tài nguyên nhanh khi server đang compute → 103 Early Hints (không liên quan real-time, chỉ giảm TTFB cho preload).
4. So sánh Polling và WebSockets
4.1 Polling
Ưu điểm:
- Dễ triển khai
- Hoạt động với mọi browser
- Không cần thay đổi server infrastructure
- Phù hợp với dữ liệu cập nhật không thường xuyên
Nhược điểm:
- Tốn bandwidth
- Độ trễ cao
- Server load lớn
- Không real-time thực sự
4.2 WebSockets
Ưu điểm:
- Real-time thực sự
- Hiệu quả về bandwidth
- Độ trễ thấp
- Giao tiếp hai chiều
Nhược điểm:
- Phức tạp hơn để triển khai
- Cần server hỗ trợ WebSocket
- Có thể gặp vấn đề với firewalls
- Cần xử lý reconnection
5. Khi nào nên sử dụng?
5.1 Sử dụng Polling khi:
- Dữ liệu cập nhật không thường xuyên
- Không yêu cầu real-time tuyệt đối
- Server infrastructure đơn giản
- Cần hỗ trợ nhiều loại client
5.2 Sử dụng WebSockets khi:
- Cần real-time thực sự
- Dữ liệu cập nhật thường xuyên
- Cần giao tiếp hai chiều
- Bandwidth là vấn đề quan trọng
6. Kết luận
Polling và WebSockets đều có vai trò riêng trong real-time communication. Việc lựa chọn phụ thuộc vào yêu cầu cụ thể của ứng dụng, infrastructure hiện có và trade-offs có thể chấp nhận được.