Xây dựng ứng dụng Real-time với WebSocket và Node.js: Từ Chat App đến Dashboard 10.000 CCU

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Bắt tay vào code: Ứng dụng Real-time trong 5 phút

Bỏ qua những trang lý thuyết dày đặc, hãy cùng mình vọc vạch ngay sức mạnh của WebSocket. Trong bài này, mình chọn Socket.io. Đây là thư viện “quốc dân” cho Node.js nhờ khả năng tự động kết nối lại và xử lý mượt mà trên mọi trình duyệt.

1. Khởi tạo project

mkdir socket-demo && cd socket-demo
npm init -y
npm install express socket.io

2. Phía Server (server.js)

const express = require('express');
const http = require('http');
const { Server } = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = new Server(server);

app.get('/', (req, res) => {
  res.sendFile(__dirname + '/index.html');
});

io.on('connection', (socket) => {
  console.log('User kết nối:', socket.id);
  
  socket.on('chat message', (msg) => {
    io.emit('chat message', msg); // Lan tỏa tin nhắn đến mọi client
  });
});

server.listen(3000, () => {
  console.log('Chạy tại http://localhost:3000');
});

3. Phía Client (index.html)

<!DOCTYPE html>
<html>
<body>
  <ul id="messages"></ul>
  <input id="input" /><button onclick="send()">Gửi</button>

  <script src="/socket.io/socket.io.js"></script>
  <script>
    const socket = io();
    function send() {
      const input = document.getElementById('input');
      socket.emit('chat message', input.value);
      input.value = '';
    }
    socket.on('chat message', (msg) => {
      const item = document.createElement('li');
      item.textContent = msg;
      document.getElementById('messages').appendChild(item);
    });
  </script>
</body>
</html>

Thử mở hai tab trình duyệt, bạn sẽ thấy tin nhắn xuất hiện tức thì ở cả hai bên. Đó chính là sức mạnh của kết nối hai chiều (Full-duplex).

WebSocket khác gì so với HTTP truyền thống?

Trước đây, để cập nhật dữ liệu, mình hay dùng Short Polling. Cứ 5 giây, client lại “hỏi” server một lần. Cách này cực kỳ ngốn tài nguyên vì mỗi request đều phải vác theo đống header, cookie và thực hiện handshake lại từ đầu.

WebSocket giải quyết triệt để vấn đề này. Nó chỉ “chào hỏi” (Handshake) qua HTTP một lần duy nhất, sau đó nâng cấp lên giao thức TCP bền vững. Lúc này, dữ liệu chảy qua lại với overhead cực thấp. Thay vì mất 1KB header cho mỗi tin nhắn, bạn chỉ tốn vài byte để duy trì kết nối.

Nâng cấp lên Dashboard giám sát chuyên nghiệp

Chuyển từ Chat App sang Dashboard giám sát đòi hỏi thay đổi tư duy. Thay vì chờ user gửi tin, server sẽ chủ động đẩy dữ liệu hệ thống dựa trên các sự kiện thực tế.

Cấu trúc Dashboard Real-time

Hãy tưởng tượng bạn cần theo dõi chỉ số CPU server theo thời gian thực. Bạn có thể dùng setInterval hoặc lắng nghe sự kiện từ Redis để đẩy về frontend.

// Đẩy thông số hệ thống mỗi 2 giây
setInterval(() => {
  const stats = {
    cpu: (Math.random() * 100).toFixed(2),
    memory: (Math.random() * 100).toFixed(2),
    time: new Date().toLocaleTimeString()
  };
  io.emit('dashboard-update', stats);
}, 2000);

Kết hợp thêm Chart.js ở frontend, bạn sẽ có một biểu đồ nhảy số liên tục mà không cần nhấn F5.

Những “hố tử thần” và bài học thực chiến

Khi scale hệ thống lên hàng chục nghìn kết nối đồng thời (CCU), mọi thứ không còn đơn giản như code demo. Dưới đây là những lỗi mình từng trả giá bằng nhiều đêm thức trắng fix bug.

1. Memory Leak vì quên dọn dẹp

Mình từng gặp dự án bị tràn RAM chỉ sau vài tiếng vận hành. Thủ phạm là do dev đăng ký listener bên trong vòng lặp hoặc không gỡ bỏ chúng khi component React bị unmount.

Giải pháp: Luôn dùng socket.off() hoặc socket.removeAllListeners() khi không còn cần thiết.

2. Xác thực (Authentication) ngay từ cửa ngõ

Nhiều bạn đợi kết nối xong mới gửi token qua sự kiện chat. Đừng làm vậy! Hãy chặn ngay từ bước Handshake bằng middleware để tránh lãng phí tài nguyên server cho những kết nối không hợp lệ.

io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  isValid(token) ? next() : next(new Error("Truy cập bị từ chối"));
});

3. Scale ngang với Redis Adapter

Một server Node.js đơn lẻ không thể chịu tải mãi. Khi bạn dùng Load Balancer để chạy 3 instance server, user ở Server A sẽ không thấy tin nhắn của user ở Server B.

Lúc này, Redis Adapter là cứu cánh. Redis đóng vai trò trung tâm điều phối tin nhắn giữa các server. Chỉ cần cấu hình thêm vài dòng, hệ thống của bạn có thể scale lên vô hạn một cách dễ dàng.

4. Xử lý trạng thái mạng chập chờn

Wifi quán cafe hay mạng 4G thường xuyên chập chờn. Socket.io có cơ chế tự kết nối lại, nhưng bạn cần xử lý UI thông minh. Hãy hiển thị loading bar hoặc thông báo “Đang kết nối lại…” để người dùng không cảm thấy ứng dụng bị treo.

Mẹo tối ưu hiệu suất

  • Dữ liệu nhị phân: Với ảnh hoặc file, hãy gửi dạng Buffer/Binary. Cách này tiết kiệm băng thông hơn nhiều so với chuỗi JSON truyền thống.
  • Namespace & Room: Đừng bao giờ broadcast cho tất cả nếu không cần. Hãy gom user vào các room (ví dụ: socket.join('order-id-123')) để tối ưu luồng tin nhắn.
  • Heartbeat: Điều chỉnh thời gian Ping/Pong hợp lý. Nếu client im lặng quá lâu, hãy ngắt kết nối ngay để giải phóng RAM cho server.

Làm Real-time không chỉ là truyền tin, mà là nghệ thuật quản lý trạng thái kết nối. Nếu bạn mới bắt đầu, hãy áp dụng Socket.io và Redis ngay. Nó sẽ giúp bạn tránh được vô vàn rắc rối khi lượng người dùng tăng trưởng nóng.

Bạn đang gặp lỗi CORS hay bị disconnect liên tục? Hãy để lại comment, mình sẽ cùng bạn “bắt bệnh” dựa trên kinh nghiệm thực chiến của mình.

Share: