Đ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

WebSocket vs Socket.io: Khi nào nên dùng cho chat, live dashboard và multiplayer?

Ếch Trendy
Ếch Trendy

15 giờ trước · 10 phút đọc

Mình đã dùng sai công nghệ suốt 6 tháng

Hồi build chat app đầu tiên, mình chọn Socket.io vì... thấy mọi tutorial đều dùng nó. Sau đó khi scale lên, mình mới nhận ra mình đang kéo theo một đống abstraction không cần thiết - fallback polling, namespace overhead, binary encoding - trong khi WebSocket thuần đã đủ dùng từ đầu.

Ngược lại, lần sau mình tự tin dùng WebSocket thuần cho một multiplayer game nhỏ. Kết quả? Mất 2 tuần tự implement reconnection logic, room management, broadcast - những thứ Socket.io đã làm sẵn.

Vấn đề không phải "cái nào tốt hơn". Vấn đề là bạn đang build gì, chạy trên infra nào, và cần trade-off nào. Bài này mình sẽ đi thẳng vào từng use case.

347419 Chọn sai công nghệ từ đầu tốn thời gian refactor hơn là học kỹ trước

WebSocket là protocol tầng transport - một TCP connection full-duplex, persistent giữa client và server. Socket.io là thư viện xây trên WebSocket (và có thể fallback sang polling) với nhiều tính năng bổ sung.

Nói đơn giản: WebSocket là con đường, Socket.io là xe có GPS, điều hòa, và tự động lái.


WebSocket hoạt động như thế nào?

Trước khi so sánh, cần hiểu WebSocket thực sự làm gì bên dưới.

Handshake: Bắt đầu bằng HTTP request thông thường với header Upgrade: websocket. Server đồng ý, connection được "nâng cấp" lên WebSocket protocol. Từ đây, HTTP không còn được dùng nữa.

// Client khởi tạo WebSocket connection
const ws = new WebSocket('wss://api.example.com/ws');

// Các event chính cần handle
ws.onopen = () => {
  console.log('Kết nối thành công');
  ws.send(JSON.stringify({ type: 'auth', token: 'abc123' }));
};

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Nhận được:', data);
};

ws.onclose = (event) => {
  // event.code: 1000 = đóng bình thường, 1006 = mất kết nối đột ngột
  console.log('Đóng với code:', event.code);
};

ws.onerror = (error) => {
  console.error('Lỗi WebSocket:', error);
};

347420 Handshake chỉ xảy ra một lần - sau đó connection tồn tại đến khi bị đóng

Frame: Dữ liệu được truyền dưới dạng frames - có thể là text (UTF-8) hoặc binary. Overhead mỗi frame chỉ 2-14 bytes header, so với HTTP polling thì nhỏ hơn nhiều lần.

Close: Quá trình đóng có 2 bước - một bên gửi close frame, bên kia acknowledge. Clean shutdown tránh được "half-open connection" để sót trên server.

Một điểm hay: WebSocket API có sẵn trên mọi browser hiện đại và Node.js. Không cần thư viện gì thêm nếu chỉ cần transport layer.


Socket.io thêm gì lên trên WebSocket?

Socket.io không phải WebSocket wrapper đơn thuần. Nó là một abstraction layer với khá nhiều thứ built-in.

Fallback mechanism: Nếu WebSocket không khả dụng (proxy chặn, corporate firewall), Socket.io tự động fallback sang HTTP long-polling. Trong thực tế năm 2024, 97% global browser support for WebSocket in 2024 (Can I use / Global browser statistics) trình duyệt hỗ trợ WebSocket nên fallback ít được dùng tới - nhưng vẫn là safety net tốt.

Reconnection logic: Đây là điểm mình thấy hữu ích nhất. Socket.io tự handle exponential backoff khi mất kết nối, không cần tự implement.

// Socket.io tự xử lý reconnection với config này
const socket = io('https://api.example.com', {
  // Thử kết nối lại tối đa 5 lần
  reconnectionAttempts: 5,
  // Thời gian chờ ban đầu giữa các lần thử (ms)
  reconnectionDelay: 1000,
  // Tăng dần delay theo exponential, tối đa 5s
  reconnectionDelayMax: 5000,
});

socket.on('connect', () => console.log('Connected:', socket.id));
socket.on('disconnect', (reason) => {
  // 'io server disconnect' = server chủ động đóng
  // 'transport close' = mất mạng
  console.log('Ngắt kết nối:', reason);
});

Rooms và namespaces: Đây là tính năng khiến Socket.io shine cho chat app.

// Server - quản lý rooms
io.on('connection', (socket) => {
  // Join vào room khi user vào kênh chat
  socket.on('join-room', (roomId) => {
    socket.join(roomId);
    // Broadcast cho tất cả trong room (trừ người gửi)
    socket.to(roomId).emit('user-joined', { id: socket.id });
  });

  // Gửi tin nhắn vào room
  socket.on('message', ({ roomId, text }) => {
    // emit tới TẤT CẢ trong room kể cả người gửi
    io.to(roomId).emit('message', {
      from: socket.id,
      text,
      timestamp: Date.now(),
    });
  });
});

347421 Rooms giúp broadcast có chọn lọc - không phải ai cũng nhận mọi message

Binary support, acknowledgments, middleware: Socket.io còn có callback-based acknowledgment (biết message đã được nhận chưa), middleware cho auth, và hỗ trợ gửi binary data như ArrayBuffer hay Blob.

Trade-off? Bundle size. Socket.io client khoảng 14.7 kB min+gzip (Socket.IO Official Documentation) - nặng hơn đáng kể so với WebSocket API native. Với app mobile hay kết nối chậm, đây là con số cần tính.


So sánh theo use case thực tế

Lý thuyết đủ rồi. Mình đi thẳng vào từng loại app.

Chat app

Đây là use case Socket.io sinh ra để làm. Rooms, broadcasting, presence (ai đang online) - tất cả đều có sẵn.

Nếu dùng WebSocket thuần cho chat, bạn phải tự quản lý map từ userId → socket connection, tự implement broadcast logic, tự track user online/offline. Tốn thời gian không cần thiết.

Kết luận cho chat: Socket.io thắng rõ ràng trừ khi bạn có lý do cụ thể để avoid nó (bundle size, custom protocol, v.v.).

Live dashboard

Live dashboard thường theo pattern pub/sub đơn giản: server push data xuống client theo định kỳ hoặc khi có sự kiện.

// WebSocket thuần - đủ dùng cho dashboard
const ws = new WebSocket('wss://metrics.example.com/stream');

ws.onmessage = (event) => {
  const metrics = JSON.parse(event.data);
  updateChart(metrics); // Cập nhật biểu đồ
};

// Server gửi metrics mỗi giây
setInterval(() => {
  const data = JSON.stringify(getCurrentMetrics());
  // Gửi cho tất cả client đang kết nối
  clients.forEach(client => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(data);
    }
  });
}, 1000);

Dashboard ít cần bidirectional communication phức tạp - phần lớn là server → client. WebSocket thuần (hoặc thậm chí Server-Sent Events) hoàn toàn đủ dùng.

Kết luận cho dashboard: WebSocket thuần hoặc SSE. Không cần kéo Socket.io vào.

347422 Dashboard chủ yếu là server push - không cần full Socket.io overhead

Multiplayer game

Đây là use case khó nhất và cũng chia rẽ nhất.

Game cần latency thấp, state sync chặt chẽ, và thường cần custom binary protocol để giảm payload size. Socket.io encode JSON theo mặc định - không tối ưu cho game loop chạy 60 lần/giây.

// WebSocket thuần + binary encoding cho game
const ws = new WebSocket('wss://game.example.com/ws');
ws.binaryType = 'arraybuffer'; // Nhận binary thay vì text

ws.onmessage = (event) => {
  // Parse binary packet tự định nghĩa
  const buffer = new DataView(event.data);
  const messageType = buffer.getUint8(0);
  const x = buffer.getFloat32(1);
  const y = buffer.getFloat32(5);
  updatePlayerPosition(messageType, x, y);
};

// Gửi player input dạng binary (nhỏ hơn JSON ~10x)
function sendPlayerInput(x, y, action) {
  const buffer = new ArrayBuffer(9);
  const view = new DataView(buffer);
  view.setUint8(0, action);     // 1 byte: loại action
  view.setFloat32(1, x);        // 4 bytes: tọa độ X
  view.setFloat32(5, y);        // 4 bytes: tọa độ Y
  ws.send(buffer);
}

Kết luận cho game: WebSocket thuần với binary encoding nếu cần tối ưu latency. Socket.io nếu game turn-based hoặc không cần optimize đến mức đó.


Scaling: khi có nhiều hơn một server

Một server thì đơn giản. Vấn đề bắt đầu khi bạn cần chạy nhiều instance.

Giả sử user A connect vào Server 1, user B connect vào Server 2. User A gửi message vào room "general" - làm sao Server 1 biết để forward cho user B trên Server 2?

Đây là lúc Redis adapter xuất hiện.

// Server - setup Socket.io với Redis adapter
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';

const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();

await Promise.all([pubClient.connect(), subClient.connect()]);

// Gắn Redis adapter vào Socket.io instance
io.adapter(createAdapter(pubClient, subClient));

// Bây giờ broadcast hoạt động across nhiều server instance
io.to('general').emit('message', { text: 'Hello từ bất kỳ server nào' });

347423 Redis là "relay" trung gian giúp các server instance sync với nhau

Với WebSocket thuần, bạn phải tự implement cơ chế tương tự - thường là dùng Redis Pub/Sub trực tiếp hoặc message broker như Kafka/RabbitMQ.

Load balancing cũng cần lưu ý: WebSocket connection cần sticky session (một client luôn được route tới cùng một server instance) trừ khi bạn có state sync hoàn hảo. Với Nginx:

upstream websocket_backend {
    # ip_hash đảm bảo cùng client → cùng server
    ip_hash;
    server backend1:3000;
    server backend2:3000;
}

server {
    location /socket.io/ {
        proxy_pass http://websocket_backend;
        # Headers bắt buộc cho WebSocket upgrade
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        # Tăng timeout để connection không bị đóng sớm
        proxy_read_timeout 3600s;
    }
}

Nếu bạn muốn đi sâu hơn vào phần scaling và infrastructure, khóa DevOps Nâng Cao có cover chi tiết load balancing và các pattern này trong môi trường production.


Decision tree: chọn cái nào?

Mình tóm gọn thành 5 câu hỏi. Trả lời theo thứ tự.

1. Browser cũ (IE11) hay môi trường lạ phải support không? → Có: dùng Socket.io (fallback tự động) → Không: tiếp tục

2. Cần rooms/namespaces/broadcast có chọn lọc không? → Có và không muốn tự code: dùng Socket.io → Không hoặc tự code được: tiếp tục

3. App có yêu cầu latency thấp (game, trading) hay custom binary protocol không? → Có: WebSocket thuần → Không: tiếp tục

4. Team có quen WebSocket API chưa? → Chưa quen và deadline gấp: Socket.io (DX tốt hơn nhiều) → Quen rồi: WebSocket thuần hoặc Socket.io đều được

5. Bundle size có quan trọng không (mobile web, kết nối chậm)? → Quan trọng: WebSocket thuần → Không: cả hai đều okay

347424 Decision tree này cover được ~90% use case thực tế

Deployment best practices

Luôn dùng wss:// (WebSocket Secure) thay vì ws:// trên production. Tương tự HTTPS vs HTTP - không có lý do gì để skip.

Implement heartbeat/ping-pong để phát hiện connection zombie:

// Server - gửi ping mỗi 30s, timeout sau 5s không có pong
const HEARTBEAT_INTERVAL = 30000;
const CLIENT_TIMEOUT = 5000;

wss.on('connection', (ws) => {
  ws.isAlive = true;
  ws.on('pong', () => { ws.isAlive = true; }); // Client phản hồi
});

const interval = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (!ws.isAlive) {
      return ws.terminate(); // Đóng connection zombie
    }
    ws.isAlive = false;
    ws.ping(); // Gửi ping
  });
}, HEARTBEAT_INTERVAL);

Rate limiting - WebSocket connection liên tục nên cần giới hạn số message/giây per client để tránh abuse.

Tài nguyên tham khảo thêm: MDN WebSocket API Reference