Bỏ qua đến nội dung chính
SSEWebSocketreal-timestreamingbackend

Server-Sent Events vs WebSocket: Chọn Cái Nào Cho Real-Time?

Server-sent events vs WebSocket: so sánh chi tiết, code Node.js minh hoạ, khi nào dùng SSE, khi nào WebSocket cho real-time app của anh.

Xuất bản 8 phút đọc

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

  1. Long polling (cũ): client request, server giữ connection mở chờ event, khi có thì trả → client request lại. Lãng phí, latency cao.
  2. Server-Sent Events (HTML5): one-way streaming server → client qua HTTP.
  3. 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ạnhSSEWebSocket
DirectionServer → ClientBidirectional
ProtocolHTTP (text/event-stream)ws:// hoặc wss://
ReconnectBuilt-in browserTự implement
Last-Event-ID resumeKhông (tự handle)
Format messageText onlyText + Binary
HTTP/2 multiplexKhông (cần Extended CONNECT)
Proxy/firewallHTTP friendlyĐôi khi bị block
Browser supportTất cả modern browserTất cả
AuthHTTP cookie/headerSubprotocol/query (cookie không gửi)
CompressionHTTP gzippermessage-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.

Zalo