Giới thiệu
Trong các ứng dụng Node.js thời gian thực, một kết nối WebSocket không chỉ là một "ống mở". Đó là một thực thể sống mở, gửi dữ liệu, có thể chết âm thầm và có thể gặp lỗi ở những cách không mong đợi. Nếu mã của bạn không quản lý rõ ràng từng giai đoạn của vòng đời đó, máy chủ của bạn sẽ rò rỉ bộ nhớ, tích tụ các kết nối zombie và cuối cùng có thể sập. Bài học này đề cập toàn bộ vòng đời: sự kiện mở, sự kiện đóng, sự kiện lỗi, và ping heartbeat được dùng để phát hiện các peer chết.
Vòng đời kết nối
Mỗi kết nối WebSocket trong Node.js trải qua bốn trạng thái quan sát được. Thư viện (ở đây chúng ta dùng gói `ws` phổ biến) phát ra các sự kiện bạn có thể lắng nghe: `connection`, `message`, `close`, và `error`. Một mẫu phổ biến là đóng gói toàn bộ trình xử lý trong một hàm duy nhất để mỗi client mới đều nhận được cùng một quản lý vòng đời.
Mã đóng `code` tuân theo đặc tả WebSocket. Các giá trị phổ biến: `1000` nghĩa là đóng bình thường do một phía yêu cầu, `1001` nghĩa là client đang điều hướng đi, `1006` là "đóng bất thường" nổi tiếng thường cho thấy mạng bị mất đột ngột.
Heartbeats: Phát hiện khách hàng chết
TCP không cho bạn biết khi một client biến mất âm thầm — kết nối chỉ ở đó, bán nổ, cho đến khi có thứ gì đó nhận ra. Giao thức WebSocket giải quyết điều này bằng các khung điều khiển gọi là ping (opcode0x9) và pong (opcode 0xA). Bên nào cũng có thể gửi ping; peers phải trả lời bằng pong chứa cùng payload. Nếu không nhận được pong trong khoảng thời gian timeout, kết nối được cho là chết và nên bị chấm dứt.
Thư viện `ws` expose `ws.ping()`, `ws.pong()`, và một tuỳ chọn `heartbeat` tích hợp sẵn làm phần lớn công việc. Mẫu dưới đây là triển khai mang tính chuẩn mắc.
Cách thức hoạt động ở thực tế: khi server khởi động, `isAlive` được đặt thành `true` cho mọi client mới. Mỗi 30 giây, server duyệt qua tất cả các client. Nếu một client đã bị đánh dấu là chết ở vòng trước, `terminate()` được gọi để đóng cứng socket TCP cơ sở. Ngược lại, cờ được reset và một frame ping được gửi đi. Một client khỏe tự động phản hồi bằng pong, điều này kích hoạt trình xử lý `pong` và đặt lại `isAlive` về `true`. Một client đã chết sẽ không bao giờ gửi pong, vì vậy ở vòng tiếp theo nó sẽ bị terminate.
Xử lý lỗi một cách lịch sự
Sự kiện `error` nảy sinh trong các trường hợp vi phạm giao thức, khung dữ liệu không hợp lệ và lỗi ở cấp thấp của socket. Nếu không có listener, sự cố lỗi không được xử lý sẽ làm crash quá trình Node.js của bạn. Luôn gắn một listener và ghép nó với sự kiện `close` vì trong hầu hết các trường hợp một lỗi được theo sau ngay bởi đóng kết nối. Một quy tắc hữu ích: ghi log lỗi để chẩn đoán, nhưng không cố gắng `send()` từ bên trong trình xử lý lỗi — socket có thể đã trở nên unusable.
Thuộc tính `code` của lỗi thường chứa các giá trị ở mức hệ thống như `ECONNRESET` hoặc `EPIPE`. Đây là gợi ý gỡ lỗi, không phải logic ứng dụng để phản hồi.
Tóm tắt
Quản lý vòng đời của WebSocket là một thói quen gồm ba phần. Luôn lắng nghe `close` và `error` để dọn dẹp các zombie và thất bại. Triển khai một vòng heartbeat ping/pong với một khoảng thời gian hợp lý (30 giây là tiêu chuẩn) để phát hiện các ngắt kết nối âm thầm. Và tuyệt đối không để sự kiện `error` bị xử lý thất lạc dẫn đến hành vi crash mặc định của Node.js. Cùng với nhau, những mẫu này giữ cho các máy chủ thời gian thực có thời gian hoạt động lâu dài ổn định ngay cả trong điều kiện mạng chập chờn và hàng nghìn client đồng thời.
Bài kiểm tra cuối bài
Ôn lại kiến thức, thử sức và nhận góp ý cho bài làm.