Khi cần real-time push từ server, lựa chọn thường gặp là Server-Sent Events vs WebSocket. Bài này so sánh đầy đủ kèm code Node.js, để anh biết khi nào dùng cái nào — đa số case SSE đủ và đơn giản hơn.
Bối cảnh: 3 thế hệ real-time
- Long polling (cũ): client request, server giữ connection mở chờ event, khi có thì trả → client request lại. Lãng phí, latency cao.
- Server-Sent Events (HTML5): one-way streaming server → client qua HTTP.
- WebSocket: bidirectional full-duplex, protocol riêng (ws://, wss://).
SSE — implement Node.js
// Server: Express SSE endpoint
app.get('/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
res.flushHeaders()
// Heartbeat 30s — chống proxy timeout (60-120s)
const heartbeat = setInterval(() => res.write(': hb\n\n'), 30000)
const subscriber = (event) => {
res.write(`id: ${event.id}\n`)
res.write(`event: ${event.type}\n`)
res.write(`data: ${JSON.stringify(event.payload)}\n\n`)
}
eventBus.on('user_update', subscriber)
req.on('close', () => {
clearInterval(heartbeat)
eventBus.off('user_update', subscriber)
})
})
// Client: browser
const es = new EventSource('/events')
es.addEventListener('user_update', (e) => {
const data = JSON.parse(e.data)
console.log('update:', data)
})
es.onerror = () => console.log('reconnecting...') // browser tự reconnect
Browser tự gửi Last-Event-ID header khi reconnect → server có thể resume từ event cuối client nhận được. Built-in resilience cực mạnh.
WebSocket — implement Node.js (ws library)
npm install ws
// Server
import { WebSocketServer } from 'ws'
const wss = new WebSocketServer({ port: 8080 })
wss.on('connection', (ws, req) => {
const userId = authenticate(req) // verify JWT từ query/header
if (!userId) return ws.close(1008, 'Unauthorized')
ws.on('message', (data) => {
const msg = JSON.parse(data.toString())
if (msg.type === 'chat') broadcastChat(msg.text, userId)
if (msg.type === 'typing') broadcastTyping(userId)
})
// Heartbeat
ws.isAlive = true
ws.on('pong', () => { ws.isAlive = true })
})
setInterval(() => {
wss.clients.forEach(ws => {
if (!ws.isAlive) return ws.terminate()
ws.isAlive = false
ws.ping()
})
}, 30000)
// Client: browser
const ws = new WebSocket('wss://api.alodev.vn/ws')
ws.onopen = () => ws.send(JSON.stringify({ type: 'subscribe', room: 'general' }))
ws.onmessage = (e) => {
const msg = JSON.parse(e.data)
console.log(msg)
}
// Reconnect logic phải tự implement
let retries = 0
ws.onclose = () => {
setTimeout(() => connect(), Math.min(30000, 1000 * Math.pow(2, retries++)))
}
So sánh chi tiết
| Khía cạnh | SSE | WebSocket |
|---|---|---|
| Direction | Server → Client | Bidirectional |
| Protocol | HTTP (text/event-stream) | ws:// hoặc wss:// |
| Reconnect | Built-in browser | Tự implement |
| Last-Event-ID resume | Có | Không (tự handle) |
| Format message | Text only | Text + Binary |
| HTTP/2 multiplex | Có | Không (cần Extended CONNECT) |
| Proxy/firewall | HTTP friendly | Đôi khi bị block |
| Browser support | Tất cả modern browser | Tất cả |
| Auth | HTTP cookie/header | Subprotocol/query (cookie không gửi) |
| Compression | HTTP gzip | permessage-deflate |
Khi nào dùng SSE?
- Push notification (in-app alert, email arrived)
- Live feed (Twitter timeline, news ticker)
- Server log streaming, build progress
- Stock price, sport score
- AI streaming response (ChatGPT-style typewriter)
Tất cả đều một chiều — server push, client chỉ nhận. SSE đơn giản, nhẹ, browser handle 90% logic.
Khi nào dùng WebSocket?
- Chat real-time (typing indicator, presence)
- Multiplayer game, sync state nhanh
- Collaborative editor (Google Docs style)
- Live trading (order/book/match symmetric)
- WebRTC signaling
Bidirectional là điều kiện cần. Nếu app anh client chỉ push 1-2 event/phút thì REST + SSE thường đơn giản hơn WebSocket full duplex.
Scale: cả hai cần Redis pub/sub
Single instance không scale. Khi anh có 3 server Node.js, user A connect server 1, user B connect server 2 — broadcast cần qua message broker:
import { createClient } from 'redis'
const pub = createClient(); await pub.connect()
const sub = pub.duplicate(); await sub.connect()
await sub.subscribe('events', (msg) => {
const event = JSON.parse(msg)
// Broadcast cho mọi local connection
for (const client of wss.clients) client.send(msg)
})
// Publish khi có event
await pub.publish('events', JSON.stringify({ type: 'chat', text: 'hi' }))
Reverse proxy (nginx, Cloudflare) cần config:
- Sticky session cho WebSocket (route same client cùng backend)
- Buffering off cho SSE (proxy_buffering off; trong nginx)
- Timeout cao (60s+) cho idle connection
Kết luận
Server-Sent Events vs WebSocket — đa số trường hợp anh chỉ cần push từ server, SSE thắng vì đơn giản và resilience tốt hơn. WebSocket cần khi bidirectional thực sự. Đừng dùng WebSocket vì "cool" — operational cost cao hơn. Tham khảo gRPC streaming nếu anh build service-to-service nội bộ cần streaming có schema chặt.