WebSocket và Socket.io: kỹ thuật realtime cho chat, live dashboard và multiplayer web app

3 tháng trước · 9 phút đọc
Tại sao HTTP không đủ cho realtime?
Bạn có bao giờ build một trang dashboard cập nhật giá cổ phiếu, rồi nhận ra mình đang dùng setInterval gọi API mỗi 1 giây? Hoặc làm chat app mà tin nhắn phải F5 mới hiện?
Vấn đề nằm ở bản chất của HTTP: request-response. Client hỏi, server trả lời, kết nối đóng. Muốn data mới, client phải hỏi lại. Cứ thế lặp.
Polling (liên tục gọi API) hoạt động được, nhưng tốn tài nguyên khủng khiếp. Hàng chục đến hàng trăm request thừa trên mỗi kết nối mỗi phút khi dùng polling 1s/lần - trong khi 99% chúng là "không có gì mới". Với 1000 người dùng đồng thời, server của bạn đang xử lý lượng traffic vô nghĩa.
HTTP polling vs WebSocket - cùng data, traffic khác nhau hoàn toàn
WebSocket ra đời để giải quyết đúng bài toán này. Thay vì mỗi lần cần data lại mở một kết nối mới, WebSocket duy trì một kết nối TCP duy nhất, hai chiều, từ khi client connect đến khi disconnect. Server push data xuống bất cứ lúc nào - không cần client hỏi.
WebSocket thuần: đơn giản hơn bạn nghĩ
WebSocket không phải thư viện hay framework - đó là giao thức chuẩn (RFC 6455), được hỗ trợ native trên mọi trình duyệt hiện đại và Node.js. Bạn không cần cài gì thêm để bắt đầu.
Phía client:
Phía server (Node.js với thư viện ws):
WebSocket thuần đủ dùng cho nhiều use case - không cần thêm dependency
WebSocket thuần phù hợp khi:
- Use case đơn giản: chat 1-1, notification đẩy xuống
- Bạn muốn kiểm soát hoàn toàn protocol
- Bundle size là ưu tiên (không muốn thêm 40KB cho Socket.io)
- Môi trường không có Node.js (Go, Rust, Python server)
Giới hạn của WebSocket thuần: bạn phải tự xử lý reconnect logic, room management, event naming, fallback khi môi trường không hỗ trợ WebSocket (proxy cũ, corporate firewall). Đây là lúc Socket.io tỏa sáng.
Socket.io: khi nào đáng thêm dependency?
Socket.io không phải WebSocket - đây là điểm nhiều người hiểu nhầm. Socket.io là một abstraction layer chạy trên WebSocket (và fallback sang long-polling nếu cần). Nó thêm:
- Event-based API: thay vì parse JSON thủ công, bạn emit/on theo tên event
- Auto-reconnect: tự động thử lại khi mất kết nối, có backoff
- Room & namespace: quản lý nhóm client dễ dàng
- Acknowledgement: biết chính xác server đã nhận message chưa
- Fallback transport: hoạt động ngay cả khi WebSocket bị block
Socket.io giải quyết phần boilerplate mà WebSocket thuần bỏ ngỏ
So sánh thực tế:
| WebSocket thuần | Socket.io | |
|---|---|---|
| Bundle size | ~0KB | ~40KB (client) |
| Reconnect | Tự code | Có sẵn |
| Room management | Tự code | socket.join() |
| Fallback | Không | Long-polling |
| Event naming | Tự parse JSON | emit/on |
| Acknowledgement | Tự code | Có sẵn |
Kết luận thực tế: Nếu build production app với nhiều người dùng, cần room, reconnect, và team không muốn tự viết boilerplate - dùng Socket.io. Nếu build microservice internal, IoT, hoặc cần tối ưu tối đa - WebSocket thuần.
Kiến trúc room và broadcast trong thực tế
Room là khái niệm quan trọng nhất khi build chat hoặc multiplayer game. Hiểu đơn giản: room là một nhóm socket ID được gắn một cái tên. Server có thể broadcast tới toàn bộ nhóm chỉ bằng một lệnh.
Ba kiểu emit cần nhớ:
Ví dụ thực tế - live dashboard cập nhật cho từng team:
Room giúp server chỉ push data tới đúng người cần - không broadcast toàn bộ
Namespace là tầng cao hơn room - dùng khi một server phục vụ nhiều ứng dụng khác nhau. Ví dụ /chat và /dashboard là 2 namespace riêng, không ảnh hưởng nhau.
Một pattern hữu ích: presence tracking - biết ai đang online trong room:
Reconnect và xử lý mất kết nối
Mạng di động chập chờn. Laptop ngủ. Proxy timeout. Mất kết nối WebSocket là chuyện bình thường, không phải edge case. Ứng dụng production phải handle được điều này.
Socket.io có auto-reconnect mặc định, nhưng bạn vẫn cần cấu hình đúng:
Rejoin room sau reconnect là bước hay bị quên, gây bug khó tìm
Với WebSocket thuần, bạn phải tự code reconnect logic:
Một điểm hay bị bỏ qua: message queue khi offline. Nếu user gõ tin nhắn lúc mất mạng, ứng dụng cần lưu tạm và gửi lại sau khi reconnect - không phải drop luôn.
Scaling: khi một server không đủ
WebSocket giữ kết nối liên tục, nghĩa là scaling horizontal không đơn giản như REST API. Vấn đề cổ điển: user A kết nối vào server 1, user B kết nối vào server 2 - khi A emit, server 1 không biết B đang ở đâu.
Không có adapter, 2 server WebSocket không nói chuyện được với nhau
Giải pháp phổ biến nhất: Redis Adapter cho Socket.io.
Với Redis Adapter, tất cả server instance dùng Redis như message bus trung tâm. Server 1 broadcast → Redis → Server 2 nhận và forward tới client. Vài mili giây latency thêm vào mỗi message - chấp nhận được cho hầu hết use case.
Kiến trúc tổng thể cho production:
[Client] ──→ [Load Balancer (sticky sessions)] ──→ [Server 1]
──→ [Server 2] ──→ [Redis]
──→ [Server 3]
Sticky sessions là bắt buộc - load balancer phải đảm bảo cùng một client luôn hit cùng một server (ít nhất trong quá trình handshake).
Các option thay thế Redis:
- Socket.io with MongoDB Adapter: dùng MongoDB Change Streams
- Ably / Pusher: managed WebSocket service, không cần tự quản lý infrastructure
- Cloudflare Durable Objects: kiến trúc edge, phù hợp app toàn cầu
Nếu bạn đang học Node.js và muốn đi sâu hơn về backend architecture, khóa Node & ExpressJS của F8 cover các khái niệm liên quan như event loop, async/await và cách build RESTful API - nền tảng tốt trước khi tackle WebSocket scaling.
