WebSocket Và Socket.io: Xây Realtime Tính Năng Live Cho Web App

5 tháng trước · 7 phút đọc
Tại sao realtime lại quan trọng
Hôm nay, người dùng không chịu chờ đợi. Chat app phải update ngay, dashboard phải hiển thị dữ liệu sống. Nếu dùng HTTP polling (gọi server mỗi 500ms), bạn sẽ phí dữ liệu và server sẽ chết. Realtime là yêu cầu, không phải tính năng bonus.
WebSocket và Socket.io là 2 công cụ chính để làm điều này. Nhưng chúng khác nhau, và việc chọn sai sẽ dẫn tới production nightmare.
HTTP polling (cũ) vs WebSocket (mới) - chênh lệch overhead rõ ràng
Bài này mình sẽ giải thích từng công cụ, cách chọn, cách deploy, và các lỗi thường gặp khi triển khai realtime.
WebSocket thuần: Cơ bản nhưng đơn giản
WebSocket là protocol HTTP upgrade. Client gửi request Upgrade, server chấp nhận, đóng kết nối HTTP, mở kết nối TCP hai chiều.
Uu điểm:
- Nhẹ, spec tiêu chuẩn W3C
- Latency cực kỳ thấp (10-20ms)
- Server có thể push data xuống client mà không cần client gửi request trước
Nhược điểm:
- Không tự động reconnect nếu mất kết nối
- Không có concept "room" - phải tự quản lý
- Không xử lý fallback nếu server không support WebSocket
- Scaling khó: nếu có 2 server, kết nối từ client A trên server 1 không biết client B trên server 2
Code WebSocket đơn giản:
Server phải tự quản lý broadcast nếu không dùng Socket.io - rất dễ sai lầm
Nhìn có vẻ đơn giản, nhưng vấn đề bắt đầu khi:
- Client disconnect đột ngột (WiFi mất) - server vẫn giữ connection 30 giây trước khi detect
- Muốn gửi message chỉ cho user X, không phải broadcast tất cả
- Deploy 2 server, 2 database - connection từ client A trên server 1 gửi message, client B trên server 2 không nhận được
Dây là lúc Socket.io vào cuộc.
Socket.io: WebSocket + nhiều tiện ích
Socket.io là thư viện wrapper trên WebSocket. Nó không tự implement WebSocket mà dùng nó nếu browser support, còn fallback sang long-polling hoặc polling.
Thêm vào đó:
- Tự động reconnect (exponential backoff)
- Event-driven API (dễ dùng hơn message string thuần)
- Room & namespace (quản lý group client)
- Middleware support
- Message acknowledgment
- Broadcasting helpers
Code Socket.io:
Socket.io server: join room, emit to room, clean API
Bạn thấy khác biệt không? Socket.io có join(), to(), event name thay vì raw string. Dễ manage hơn.
Với WebSocket thuần, nếu muốn join room, bạn phải:
- Client send message {type: 'join_room', roomId: 'xxx'}
- Server parse message, check type
- Server quản lý object {roomId: [clientList]}
- Khi broadcast, loop qua list này
Socket.io làm hết điều này cho bạn. Trade-off? Thêm một lượng metadata nhỏ bytes overhead vì Socket.io gửi kèm metadata.
Room, Namespace, Reconnect: Socket.io nâng cao
Socket.io có 3 concept quan trọng.
Room: Một tập hợp socket bên trong namespace. Dùng socket.join(room), io.to(room).emit(event, data).
Namespace: Như subdomain socket. Dùng khi có nhiều feature độc lập (chat, notification, dashboard).
Reconnect: Socket.io tự động reconnect với exponential backoff (1s, 2s, 4s, ..., max 1 phút).
3 concept: room (nhóm trong namespace), namespace (subdomain), reconnect (tự động kết nối lại)
Nhưng có 1 vấn đề: Nếu client disconnect 5 phút, server sẽ xóa session của user đó. Khi reconnect, user phải login lại. Cách xử lý:
- Client cache sessionId/token
- Server check token, restore session nếu còn valid
- Client emit 'rejoin' event
Scaling realtime: Red is, cluster, queue
Khi có 1000+ concurrent user, 1 server không đủ. Vấn đề: user A connect server 1, user B connect server 2. Khi A send message, B không nhận vì message chỉ tới server 1.
Giải pháp: Adapter.
Socket.io mặc định dùng in-memory adapter. Có thể swap sang Redis:
Now, message từ server 1 sẽ publish tới Redis, server 2 subscribe và forward tới user. Tất cả server như 1 hệ thống.
Cấu trúc scaling: 2 server, 1 Redis pub/sub - message từ server 1 tới server 2 qua Redis
Alternative: Message queue (RabbitMQ, Kafka). Nhưng Socket.io Redis adapter đã đủ cho 90% use case.
Vấn đề khác: Persistent message. Nếu user offline 5 phút, họ nhận không các message lúc offline. Giải pháp:
- Database: lưu message, khi user online, query message từ lúc họ offline
- Message queue: RabbitMQ dead letter queue, keep message 7 ngày
Mình recommend cách 1 (database) vì simple và cost thấp.
Lỗi thường gặp khi triển khai realtime
1. Memory leak từ event listener
Khi user disconnect, bạn quên remove listener.
Hoặc đơn giản, Socket.io tự động clean khi user disconnect, nên không cần lo.
2. Lặp vô hạn emit
User A send message → Server broadcast → User A nhận → emit lại → broadcast → lặp vô hạn.
3. Không xử lý connection timeout
Server mất kết nối nhưng client chưa detect. Timeout setting:
4. Không validate message từ client
Bạn đã bao giờ tìm thấy chat app bị spam 10000 message trong 1 giây chưa? Đó là lý do.
Một đoạn code nhỏ validate có thể chặn được 90% attack
5. Không rate limit
Thêm rate limit vào socket event:
Hoặc dùng library: socket.io-rate-limit.
