Đang tải...

Ếch Trendy

Cùng chú ếch nhỏ khám phá thế giới Web đầy biến động. Cập nhật trending nhanh như cách ếch đớp mồi! ⌨️🌿


0

Socket.IO v4.x: Xây Realtime Chat App Với Rooms Và Broadcasting Cho Dự Án Việt

Ếch Trendy
Ếch Trendy

3 tháng trước · 11 phút đọc

Socket.IO là gì và tại sao không phải WebSocket thuần?

Bạn có bao giờ tự hỏi tại sao mình không dùng thẳng WebSocket cho ứng dụng chat, mà lại cần thêm một lớp thư viện nữa?

Câu trả lời ngắn: WebSocket thuần đòi bạn tự xử lý hầu hết mọi thứ - reconnect khi đứt mạng, fallback khi môi trường không hỗ trợ, buffer event trong lúc offline, broadcast đến nhóm người dùng. Socket.IO đóng gói tất cả phần đó lại và cho bạn một API event-based sạch hơn nhiều.

Cụ thể hơn, Socket.IO không phải là WebSocket. Nó là một lớp cao hơn: ưu tiên dùng WebSocket khi có thể, nhưng tự động rơi xuống HTTP long-polling nếu WebSocket bị chặn - điều khá phổ biến ở một số mạng doanh nghiệp và môi trường hosting rẻ tiền tại Việt Nam. Với các phiên bản gần đây trong nhánh v4.x, Socket.IO còn hỗ trợ WebTransport như một transport tùy chọn trong điều kiện phù hợp.

345772 WebSocket là nền, Socket.IO là lớp tiện ích phía trên - không nên nhầm lẫn hai thứ này

Phiên bản hiện tại đáng tin cậy nhất để dùng trong dự án thực là Socket.IO v4.x (latest stable là 4.8.x vào cuối 2025). Mình sẽ dùng v4.x làm chuẩn trong toàn bài. Nếu bạn thấy ai đó nhắc đến "Socket.IO 5.0", hãy thận trọng - chưa có đủ bằng chứng về một bản release chính thức với breaking changes rõ ràng tại thời điểm bài viết này.

Mô hình hoạt động cũng đơn giản hơn bạn nghĩ:

  1. Client kết nối đến server (namespace mặc định hoặc namespace chỉ định)
  2. Socket.IO chọn transport phù hợp nhất
  3. Hai bên trao đổi events có tên, ví dụ chat_message, typing, join_room
  4. Khi mạng chập chờn, client tự reconnect và event có thể được buffer rồi gửi lại

Đây là lý do Socket.IO phổ biến cho chat app, realtime dashboard và multiplayer game - những use case mà bạn cần reliable connection hơn là raw performance.


Cài đặt và khởi tạo dự án

Mình sẽ dùng stack đơn giản nhất có thể: Node.js + Express cho backend, HTML/JS thuần cho frontend (phần sau sẽ nói thêm về React). Không cần setup phức tạp.

# Tạo thư mục dự án
mkdir chat-app && cd chat-app
npm init -y

# Cài dependencies backend
npm install express socket.io

# Cài dependencies frontend (dev)
npm install -D nodemon

Cấu trúc thư mục:

chat-app/
├── server.js
├── public/
│   └── index.html
└── package.json

Server cơ bản nhất:

// server.js
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');

const app = express();
const httpServer = createServer(app);

// Khởi tạo Socket.IO, cấu hình CORS cho dev
const io = new Server(httpServer, {
  cors: {
    origin: 'http://localhost:3000',
    methods: ['GET', 'POST']
  }
});

app.use(express.static('public'));

io.on('connection', (socket) => {
  console.log(`User kết nối: ${socket.id}`);

  socket.on('disconnect', () => {
    console.log(`User ngắt kết nối: ${socket.id}`);
  });
});

httpServer.listen(4000, () => {
  console.log('Server chạy tại http://localhost:4000');
});

345773 Chỉ cần 3 package là đủ để có WebSocket server hoạt động

Phía client, bạn có thể dùng CDN để test nhanh:

<!-- public/index.html -->
<script src="/socket.io/socket.io.js"></script>
<script>
  // Socket.IO tự serve file client JS này
  const socket = io();

  socket.on('connect', () => {
    console.log('Đã kết nối, id:', socket.id);
  });
</script>

Một chi tiết hay: file socket.io.js được serve tự động bởi server Socket.IO, bạn không cần cài thêm gì ở frontend. Nếu dùng React hoặc bundler khác thì npm install socket.io-client và import bình thường.

Nếu bạn chưa quen với Node.js và Express, khóa Node & ExpressJS trên F8 cover đầy đủ phần nền tảng này trước khi đến Socket.IO.


Rooms: Cách gom người dùng vào phòng chat

Rooms là tính năng mình thấy nhiều bạn Việt bỏ qua khi mới học Socket.IO, rồi sau đó tự hỏi tại sao broadcast của mình lại gửi tin nhắn cho tất cả mọi người thay vì chỉ người trong phòng.

Rooms là abstraction phía server. Không có gì ở phía client biết về rooms - đây là khái niệm thuần server-side để Socket.IO biết khi emit event thì gửi cho ai.

Một socket có thể join nhiều rooms cùng lúc. Và mỗi socket mặc định đã nằm trong một room riêng có id là socket.id của nó.

// server.js - xử lý logic rooms
io.on('connection', (socket) => {

  // Client yêu cầu vào phòng
  socket.on('join_room', (roomName) => {
    socket.join(roomName);
    console.log(`${socket.id} vào phòng: ${roomName}`);

    // Thông báo cho những người đã trong phòng
    socket.to(roomName).emit('user_joined', {
      userId: socket.id,
      message: `Có người vừa vào phòng ${roomName}`
    });
  });

  // Client rời phòng
  socket.on('leave_room', (roomName) => {
    socket.leave(roomName);
    socket.to(roomName).emit('user_left', {
      userId: socket.id
    });
  });

});

345774 Mỗi room là một nhóm socket riêng biệt - tin nhắn chỉ đi trong phòng đó

Phía client:

// Vào phòng
socket.emit('join_room', 'phong-backend-viet');

// Lắng nghe sự kiện từ phòng
socket.on('user_joined', (data) => {
  console.log(data.message);
});

Bạn cũng có thể xem ai đang ở trong phòng nào từ phía server:

// Lấy danh sách socket trong một room
const socketsInRoom = await io.in('phong-backend-viet').fetchSockets();
console.log(`Số người trong phòng: ${socketsInRoom.length}`);

Use case thực tế cho dự án Việt: phòng theo team (frontend, backend, QA), phòng theo dự án, hoặc channel theo workspace kiểu Slack mini. Logic join/leave room không phức tạp, nhưng bạn cần quyết định rõ ai được join phòng nào - phần đó là business logic của bạn, không phải Socket.IO lo.


Broadcasting: Gửi tin nhắn đúng người, đúng phòng

Đây là phần hay nhất. Socket.IO có nhiều cách emit khác nhau, và chọn sai là nguyên nhân số 1 gây ra bug kiểu "tại sao tôi nhận được tin nhắn của chính mình".

Mình tổng hợp lại 4 kiểu emit phổ biến nhất:

Cách emit Gửi đến ai Dùng khi nào
socket.emit(event, data) Chỉ socket hiện tại Gửi phản hồi cho người vừa gửi
socket.to(room).emit(...) Tất cả trong room, trừ người gửi Chat message vào phòng
io.to(room).emit(...) Tất cả trong room, kể cả người gửi Thông báo hệ thống
io.emit(event, data) Tất cả mọi người kết nối Announcement toàn server

Code thực tế cho một chat room:

// server.js
io.on('connection', (socket) => {

  socket.on('join_room', (roomName) => {
    socket.join(roomName);
  });

  socket.on('chat_message', ({ room, message, username }) => {
    // Gửi đến tất cả trong phòng kể cả người gửi
    // để người gửi cũng thấy tin của mình ngay lập tức
    io.to(room).emit('receive_message', {
      username,
      message,
      timestamp: new Date().toISOString()
    });
  });

  socket.on('typing', ({ room, username }) => {
    // Chỉ báo cho người khác, không cần người đang gõ tự thấy
    socket.to(room).emit('user_typing', { username });
  });

});

345775 Chọn đúng kiểu emit là khác biệt giữa bug và feature

Phía client React:

// Gửi tin nhắn
const sendMessage = () => {
  socket.emit('chat_message', {
    room: currentRoom,
    message: inputText,
    username: myUsername
  });
  setInputText('');
};

// Nhận tin nhắn
useEffect(() => {
  socket.on('receive_message', (data) => {
    setMessages(prev => [...prev, data]);
  });

  // Cleanup khi unmount
  return () => socket.off('receive_message');
}, []);

Lưu ý cái socket.off('receive_message') trong cleanup. Quên cái này là lý do hay gặp nhất khiến component React nhận duplicate messages - mỗi lần re-render lại đăng ký thêm một listener mới mà không remove cái cũ.


Auto-reconnect và connection state recovery

Mạng Việt Nam không phải lúc nào cũng ổn định. Đây là thực tế mà bất kỳ ai build sản phẩm cho người dùng trong nước đều hiểu.

Mặc định, Socket.IO đã bật auto-reconnect - client sẽ tự thử kết nối lại theo exponential backoff khi mất mạng. Bạn không cần viết thêm gì. Nhưng có một vấn đề: trong lúc mất kết nối, những event gửi từ server sẽ bị mất.

Socket.IO v4.6+ giới thiệu connection state recovery - khi client reconnect thành công trong một khoảng thời gian ngắn, server có thể replay lại các event bị bỏ lỡ. Cấu hình phía server:

const io = new Server(httpServer, {
  // Bật connection state recovery
  connectionStateRecovery: {
    // Thời gian giữ state sau ngắt kết nối (ms)
    maxDisconnectionDuration: 2 * 60 * 1000, // 2 phút
    // Có skip middleware khi recover không
    skipMiddlewares: true
  }
});

Phía client, bạn có thể detect trạng thái reconnect:

socket.on('connect', () => {
  if (socket.recovered) {
    // Reconnect thành công, event bị lỡ đã được replay
    console.log('Kết nối lại, dữ liệu đã được khôi phục');
  } else {
    // Kết nối mới hoàn toàn, cần fetch lại dữ liệu
    console.log('Kết nối mới, cần load lại lịch sử chat');
    fetchChatHistory();
  }
});

345776 socket.recovered giúp bạn biết có cần fetch lại data hay không

Ngoài ra, bạn nên xử lý UI cho người dùng khi mất kết nối:

socket.on('disconnect', (reason) => {
  // Hiển thị banner "Đang kết nối lại..."
  setConnectionStatus('reconnecting');
});

socket.on('connect_error', (error) => {
  console.error('Lỗi kết nối:', error.message);
});

socket.io.on('reconnect', (attemptNumber) => {
  // Đã kết nối lại sau N lần thử
  setConnectionStatus('connected');
});

Một trade-off cần biết: connection state recovery yêu cầu server giữ state trong memory (hoặc adapter tương thích). Nếu bạn scale lên nhiều server instances, cần thêm Redis adapter để các instance chia sẻ state với nhau - không phải vấn đề với prototype, nhưng cần tính trước khi đưa lên production.


Demo: Chat app hoàn chỉnh với rooms

Mình tổng hợp tất cả những phần trên vào một demo có thể chạy được ngay. File HTML dưới đây là client đầy đủ, chạy được với server Node.js đã viết ở trên.

Tải file demo chat app HTML

Demo bao gồm:

  • Nhập tên và chọn/tạo phòng chat
  • Gửi và nhận tin nhắn realtime
  • Hiển thị trạng thái "đang gõ..."
  • Thông báo khi có người vào/rời phòng
  • Hiển thị trạng thái kết nối

Phần server hoàn chỉnh:

// server.js - phiên bản đầy đủ
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');

const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
  cors: { origin: '*' },
  connectionStateRecovery: {
    maxDisconnectionDuration: 2 * 60 * 1000
  }
});

// Lưu username theo socket id (in-memory, đủ cho demo)
const users = new Map();

io.on('connection', (socket) => {

  socket.on('set_username', (username) => {
    users.set(socket.id, username);
  });

  socket.on('join_room', (roomName) => {
    socket.join(roomName);
    const username = users.get(socket.id) || 'Ẩn danh';
    socket.to(roomName).emit('user_joined', { username, room: roomName });
  });

  socket.on('chat_message', ({ room, message }) => {
    const username = users.get(socket.id) || 'Ẩn danh';
    io.to(room).emit('receive_message', {
      username,
      message,
      timestamp: new Date().toLocaleTimeString('vi-VN')
    });
  });

  socket.on('typing', ({ room }) => {
    const username = users.get(socket.id);
    if (username) socket.to(room).emit('user_typing', { username });
  });

  socket.on('stop_typing', ({ room }) => {
    socket.to(room).emit('stop_typing');
  });

  socket.on('disconnect', () => {
    users.delete(socket.id);
  });

});

httpServer.listen(4000, () => console.log('Server: http://localhost:4000'));

345777 Bước tiếp theo: thêm xác thực JWT và dùng Redis adapter khi cần scale

Nếu bạn muốn đưa lên production, hai thứ cần làm ngay:

  1. Xác thực: Thêm middleware Socket.IO để verify JWT token trước khi cho phép kết nối
  2. Adapter: Dùng @socket.io/redis-adapter nếu chạy nhiều server instance
// Middleware xác thực
io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  if (!token) return next(new Error('Chưa đăng nhập'));
  // verify token ở đây
  next();
});

Khóa Node & ExpressJS và Xây Dựng Website với ReactJS trên F8 là hai nền tảng bạn cần vững trước khi tích hợp Socket.IO vào dự án thực tế - Socket.IO chỉ là tầng realtime, phần còn lại vẫn là Express routing và React state management bình thường.

Trang web dùng cookie để hoạt động và đo hiệu quả quảng cáo. Xem chính sách bảo mật