0
0
Lập trình
Sơn Tùng Lê
Sơn Tùng Lê103931498422911686980

3 Ngày Debug WebSocket: Những Bài Học Quý Giá

Đăng vào 8 tháng trước

• 5 phút đọc

Giới Thiệu

Xây dựng một ứng dụng chat tưởng chừng như rất dễ dàng. Nhưng sau ba ngày vật lộn với việc debug lỗi WebSocket, tôi đã nhận ra rằng điều đó không hề đơn giản. Tất cả chúng ta đều có thể tạo ra một ứng dụng chat đơn giản, nhưng vấn đề thực sự bắt đầu khi bạn muốn biến nó thành một ứng dụng có khả năng bảo trì tốt. Dưới đây là ba bài học mà tôi đã rút ra từ trải nghiệm này, hy vọng chúng cũng sẽ giúp ích cho bạn khi làm việc với WebSocket.

Bài Học 1: Tách Rời Rõ Ràng Giữa Tin Nhắn Từ Client và Server

Trong lần thử nghiệm đầu tiên của mình, tôi đã gộp tất cả vào một handler .on("message"). Kết quả là một mớ hỗn độn: các tin nhắn bay khắp nơi, không biết ai nói gì, và tôi thì ngập chìm trong logs.

Giải pháp thật đơn giản:

  • Client → Server: chỉ gửi các tin nhắn chat, biên nhận, sự kiện gõ.
  • Server → Client: chỉ gửi thông tin, lỗi và payload định tuyến.

Khi tách biệt các luồng này, logic định tuyến chỉ tồn tại ở nơi nó cần, và server ngừng mất kiểm soát. Việc debug đã chuyển từ “Cái quái gì thế này?” thành “À, đây là nơi nó bị lỗi.”

Mô Hình Tâm Lý Đã Giúp Tôi Hiểu

javascript Copy
ws.on("message", (data, isBinary) => {
  if (isBinary) {
    logger.info("Chúng ta có một payload nhị phân trong tin nhắn! Không xử lý cái đó");
    return;
  }

  const recievedMessage: Envelope | null = parseEnvelope(data);

  if (recievedMessage == null) {
    logger.info("Nhận được định dạng tin nhắn kỳ lạ");
    return;
  }

  switch (recievedMessage.type) {
    case "chat":
      handleChatMessages(ws, recievedMessage, userToWs, user.id);
      break;
    case "ack":
      handleAck(ws, recievedMessage, userToWs, user.id);
      break;
    default:
      logger.info("Bạn đã gửi một lựa chọn không hợp lệ.");
      break;
  }
});

Bài Học 2: Đừng Thay Đổi Schema Payload Giữa Chừng

Sai lầm lớn nhất mà tôi đã mắc phải là thêm các trường không cần thiết vào payload của mình chỉ vì “à, sửa nhanh thôi.” Kết quả là ba ngày vật lộn với những lỗi phantom, tôi nhận ra chính mình là hồn ma ám ảnh hệ thống của mình.

Nguyên tắc vàng:

  • Định nghĩa schema của bạn một lần.
  • Không bao giờ thay đổi nó trong quá trình truyền.
  • Nếu cần các trường tùy chọn → xây dựng chúng thành tùy chọn trong schema.

Tương lai của bạn sẽ cảm ơn bạn vì điều này.

javascript Copy
import { z } from "zod";

/**
 * Client → Server: Tin nhắn chat
 */
export const ChatMessageSchema = z.object({
  type: z.literal("chat"),
  to: z.string(),
  from: z.string(),
  messageId: z.string(),
  message: z.string(),
  mode: z.enum(["offline", "online"]),
  timestamp: z.number(),
  streamId: z.string().optional(),
});

/**
 * Client → Server: Xác nhận
 */
export const ChatAckSchema = z.object({
  type: z.literal("ack"),
  to: z.string(),
  from: z.string(),
  messageId: z.string(),
  timestamp: z.number(),
  streamId: z.string().optional(),
  ackType: z.enum(["read", "delivered"]),
});

/**
 * Server → Client: Thông tin hệ thống
 * Ví dụ: "bạn đã kết nối", "server đang khởi động lại", v.v.
 */
export const SystemInfoSchema = z.object({
  type: z.literal("system"),
  message: z.string(),
});

/**
 * Server → Client: Thông tin lỗi
 * Bao gồm các thành phần nội bộ / bên ngoài.
 */
export const SystemErrorSchema = z.object({
  type: z.literal("error"),
  component: z.string(),
  message: z.string(),
});

/**
 * Envelope: mọi tin nhắn vào/ra phải là một trong những này.
 */
export const EnvelopeSchema = z.union([
  ChatMessageSchema,
  ChatAckSchema,
  SystemInfoSchema,
  SystemErrorSchema,
]);

// ------------ Types ------------
export type ChatMessage = z.infer<typeof ChatMessageSchema>;
export type ChatAck = z.infer<typeof ChatAckSchema>;
export type SystemInfo = z.infer<typeof SystemInfoSchema>;
export type SystemError = z.infer<typeof SystemErrorSchema>;
export type Envelope = z.infer<typeof EnvelopeSchema>;

Bài Học 3: Ghi Lại Mọi Lần Client Ngắt Kết Nối

Đây là một điều khá đau đớn: Tôi đã thử nghiệm server socket của mình với một client React. Các kết nối liên tục ngắt quãng với mã thoát ngẫu nhiên như 10061005. Tôi nghĩ rằng server của mình bị lỗi. Tôi đã debug như một kẻ điên trong ba ngày liên tục.

Thủ phạm thực sự? Chế độ Strict của React khi gắn và ngắt kết nối socket.

Hai điều cần lưu ý:

  1. Ghi lại cách và khi nào mỗi client ngắt kết nối. Điều này sẽ tiết kiệm cho bạn hàng giờ đồng hồ.
  2. Nếu bạn đang thử nghiệm với React → hãy vô hiệu hóa Chế độ Strict hoặc chỉ sử dụng một client JS thuần.

Khi tôi thực hiện điều đó, những hồn ma biến mất và server của tôi hoạt động như nó nên có.

javascript Copy
ws.on("close", async (code, reason) => {
  console.log("❌ WS đã đóng:", code, reason.toString());

  // 1. Dọn dẹp sự hiện diện toàn cầu
  await terminateUserFromRedis(user.mobileNo);

  // 2. Dọn dẹp các bản đồ trong bộ nhớ
  userToWs.delete(user.mobileNo);  // userId → ws map
  wsToUser.delete(ws);             // ws → userId map
  activeConnection.delete(ws);     // ws → cờ liveness

  // 3. Ghi log
  logger.info(`Người dùng đã ngắt kết nối: ${user.mobileNo}`);
});

Kết Luận

Các ứng dụng chat toy rất dễ làm. Nhưng các ứng dụng chat có khả năng debug thì lại rất khó. Càng sớm bạn:

  • Tách biệt rõ ràng giữa các tin nhắn từ client và server,
  • Tôn trọng các schema của bạn,
  • Ghi log một cách thường xuyên,

thì bạn sẽ càng ít sợ WebSocket hơn, và nhanh chóng xây dựng các hệ thống mà bạn thực sự có thể tin tưởng.

Câu Hỏi Thường Gặp (FAQ)

1. WebSocket là gì?
WebSocket là một giao thức mạng cho phép kết nối hai chiều giữa client và server, giúp truyền tải dữ liệu theo thời gian thực.

2. Tại sao cần phải ghi lại các kết nối ngắt?
Việc ghi lại các kết nối ngắt giúp bạn nhận diện và xử lý các vấn đề xảy ra trong quá trình kết nối, giúp cải thiện chất lượng ứng dụng.

3. Làm thế nào để debug WebSocket hiệu quả?
Hãy tách biệt tin nhắn, giữ nguyên schema và ghi log đầy đủ các sự kiện để dễ dàng phát hiện lỗi.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào