WebSocket và Socket.io: Xây dựng realtime chat, live dashboard, multiplayer game

2 tháng trước · 9 phút đọc
Tại sao HTTP không đủ cho realtime?
HTTP hoạt động theo mô hình request-response: client hỏi, server trả lời, xong. Mỗi kết nối mở ra rồi đóng lại. Cách này ổn với blog hay REST API, nhưng với chat app thì sao?
Bạn phải liên tục poll server mỗi vài giây để kiểm tra tin nhắn mới. HTTP long polling sử dụng khoảng 7.4× lượng dữ liệu so với WebSocket (884,698 bytes vs 119,692 bytes cho 1,000 sự kiện) (Nghiên cứu so sánh HTTP polling vs WebSocket 2023-2024) lượt poll không cần thiết mỗi giây - tốn băng thông, tốn CPU, và người dùng vẫn thấy delay.
HTTP polling vs WebSocket - chi phí kết nối chênh lệch rõ rệt
WebSocket giải quyết bằng cách duy trì một kết nối persistent. Sau handshake ban đầu (vẫn qua HTTP/HTTPS), kết nối được "nâng cấp" lên giao thức WebSocket. Từ đó server có thể chủ động push data xuống client bất cứ lúc nào mà không cần client hỏi trước.
Socket.io không phải là WebSocket thuần. Nó wrap WebSocket và thêm một đống thứ quan trọng cho production: tự động reconnect, fallback sang long-polling nếu WebSocket bị chặn, rooms, namespaces, acknowledgements. Với Socket.io 5.0, engine đã được viết lại hoàn toàn - connection overhead giảm đáng kể so với v2.
Architecture cơ bản: Server và Client setup
Trước khi đi vào từng feature, mình cần có base architecture rõ ràng. Đây là setup production-ready cho một Node.js server với Socket.io 5.0.
Middleware auth chạy một lần trước handshake - hiệu quả hơn check trong mỗi event
Phía client với React:
Một điểm nhiều người bỏ qua: reconnection logic. Khi network mất rồi có lại, client cần tự reconnect và re-join các rooms. Socket.io có sẵn cơ chế này, nhưng bạn vẫn cần handle connect event để restore state (re-join room, re-subscribe data).
Rooms và Namespaces: Tổ chức connection có cấu trúc
Đây là hai khái niệm hay bị lẫn lộn.
Namespace giống như endpoint riêng biệt - /chat, /dashboard, /game. Mỗi namespace có connection pool, middleware và event handler riêng. Dùng khi bạn có nhiều "ứng dụng" trong một server.
Room là nhóm logic bên trong một namespace. Một socket có thể join nhiều rooms cùng lúc. Dùng để broadcast message đến subset của users - ví dụ channel Slack, phòng chờ trong game.
Rooms cho phép broadcast đúng nhóm - không tốn băng thông gửi cho người không liên quan
Một pattern hữu ích: Private rooms. Mỗi user join một room tên theo user ID của họ. Cần gửi notification đến user cụ thể? Emit đến room đó thay vì track socket ID (vì socket ID thay đổi sau mỗi reconnect).
Xây live dashboard với broadcasting
Live dashboard là use case điển hình: server liên tục push metrics, chart cập nhật real-time. Thay vì mỗi tab browser poll riêng, bạn aggregate data một chỗ rồi broadcast.
Subscribe theo metric type - không broadcast thừa data không ai cần
Phía React client:
Một điểm cần chú ý với dashboard: backpressure. Nếu client xử lý chậm hơn server push, data sẽ queue up và gây memory leak. Giải pháp đơn giản: chỉ giữ N data points gần nhất (như ví dụ trên), hoặc dùng volatile emit - Socket.io sẽ bỏ qua message nếu socket đang busy.
Multiplayer game: Đồng bộ state với độ trễ thấp
Game multiplayer phức tạp hơn chat vì yêu cầu state consistency - tất cả người chơi phải thấy cùng một trạng thái. Và phải nhanh.
Pattern phổ biến là server-authoritative: client gửi input (ý định), server xử lý logic và broadcast state đã validated.
Server là authority - client không được tự quyết game state để tránh cheating
Về latency: Với game turn-based (cờ, quiz), delay 100-200ms hoàn toàn chấp nhận được. Game action real-time (shooter, racing) cần phức tạp hơn: client-side prediction + server reconciliation. Đây là chủ đề riêng, nhưng Socket.io đủ dùng cho phần transport layer.
Scaling với Redis Adapter
Một Socket.io server single-process chỉ xử lý được đến một ngưỡng nhất định. Khi cần horizontal scaling (nhiều instances), vấn đề xuất hiện: user A connect vào instance 1, user B connect vào instance 2 - nếu A gửi message cho B, instance 1 không biết socket của B đang ở đâu.
Giải pháp: Redis Adapter. Tất cả instances đều sub/pub qua Redis. Khi instance 1 emit đến một room, Redis broadcast tới tất cả instances khác, instance nào có socket trong room đó sẽ deliver.
Redis làm message bus giữa các instances - scale ngang mà không cần sticky sessions
Ngoài Redis Adapter, cần chú ý thêm:
Rate limiting - tránh một client spam events:
Message queuing khi cần đảm bảo delivery: nếu user offline, message cần được lưu lại. Đơn giản nhất là lưu vào DB và sync lại khi user reconnect - kiểm tra lastSeenAt timestamp.
Khóa Node & ExpressJS của F8 cover phần Redis caching và session management khá chi tiết nếu bạn muốn nền tảng vững hơn trước khi implement adapter này.
