Tại Sao Giao Thức Gói Mà Skynet Cung Cấp Chỉ Sử Dụng 2 Byte Để Biểu Diễn Độ Dài Gói
Tại sao giao thức đóng gói của skynet lại chỉ dùng 2 byte để biểu diễn độ dài gói tin?
Trong skynet, thư viện lua netpack có chức năng phân tích luồng dữ liệu TCP thành các gói tin theo cấu trúc độ dài + nội dung. Dù người dùng không bắt buộc phải sử dụng thư viện này (như driver redis trong skynet tự thiết kế giao thức phân tách dữ liệu dựa trên ký tự xuống dòng), vẫn nhiều lập trình viên thắc mắc liệu có thể tùy chỉnh độ dài phần header của gói tin không.
Phiên bản hiện tại quy định độ dài gói tin được mã hóa theo định dạng big-endian với 2 byte, giới hạn tối đa 64KB. Điều này khiến nhiều người dùng cảm thấy bất tiện. Đã có đề xuất yêu cầu mở rộng độ dài header lên 4 byte, vì một số ứng dụng đặc thù có những gói tin vượt ngưỡng 64KB dù số lượng không nhiều.
Trong lịch sử, phiên bản gate của skynet viết bằng C từng hỗ trợ tùy chọn 2 hoặc 4 byte cho độ dài header. Tuy nhiên qua cân nhắc kỹ lưỡng, quyết định loại bỏ tính năng này nhằm đảm bảo nguyên tắc thiết kế thư viện: phải đơn giản và hướng dẫn người dùng sử dụng đúng cách, thay vì tạo cơ hội cho lỗi sai phát sinh.
Trong kết nối với client game, việc sử dụng một kết nối TCP duy nhất đòi hỏi yêu cầu nghiêm ngặt về độ trễ. Cho phép gói tin quá lớn bản thân đã là sai lầm, thậm chí với 64KB cũng đã quá mức cần thiết. Lấy ví dụ: trong môi trường mạng yếu như di động, việc xử lý gói tin 100KB có thể kéo dài hơn 1 phút, trong khi những dữ liệu lớn như vậy thường không cần phản hồi tức thời. Một tình huống điển hình là khi người dùng truy vấn toàn bộ vật phẩm đang được rao bán trên chợ game - nếu nhét toàn bộ dữ liệu vào một gói tin, kết quả dễ dẫn đến độ trễ ảnh hưởng trải nghiệm.
Việc truyền tải gói tin lớn qua kết nối TCP đơn sẽ gây tắc nghẽn toàn bộ kênh truyền. Những gói tin quan trọng cần xử lý nhanh như gói kiểm tra trạng thái mạng (heartbeat) sẽ bị chặn lại phía sau. Hơn nữa, tầng xử lý mạng thường không cung cấp API để tầng nghiệp vụ kiểm soát trạng thái nhận dữ liệu (như gateserver của skynet không hỗ trợ điều này, dù có thể thêm vào với vài dòng code lua).
Giải pháp tối ưu là xây dựng một tầng giao thức bổ sung cho phép chia nhỏ dữ liệu lớn thành nhiều gói tin. Bằng cách thêm ID xác định khối dữ liệu vào header, các gói tin sau đó chỉ cần trích dẫn ID này thay vì truyền toàn bộ dữ liệu. Cách thiết kế gián tiếp này phản ánh việc bạn đã nhận thức rõ về vấn đề đang giải quyết.
Việc chia nhỏ gói tin còn mở ra khả năng truyền tải đa kênh trên một kết nối TCP duy nhất. Trong game online, các luồng dữ liệu thường không phụ thuộc lẫn nhau - như thông tin chat và dữ liệu đồng bộ hóa cảnh quan hoàn toàn độc lập. Skynet hỗ trợ tính năng này qua API socket đặc biệt: hàm socket.lwrite cho phép gửi dữ liệu vào kênh ưu tiên thấp. Chỉ khi các gói tin trong kênh cao ưu tiên (mặc định) được xử lý xong, hệ thống mới bắt đầu xử lý gói tin ưu tiên thấp, đảm bảo tính nguyên tử cho từng gói.
Ví dụ: bạn có thể dùng kênh ưu tiên thấp để gửi tin nhắn chat, tránh tình trạng nghẽn mạng làm chậm các gói tin quan trọng khác. Hay chia nhỏ dữ liệu lớn gửi qua kênh này, cho phép xen kẽ với các luồng dữ liệu quan trọng khác. Thậm chí bạn có thể gửi toàn bộ dữ liệu cho client qua kênh ưu tiên thấp, chỉ giữ lại gói heartbeat ở kênh cao ưu tiên để đảm bảo tần suất kiểm tra ổn định mạng.
Ngoài ra, việc sử dụng 4 byte cho độ dài header tiềm ẩn lỗ hổng bảo mật. Khi nhận header, nhiều đoạn code phân tích gói tin thường cấp phát bộ nhớ theo độ dài khai báo. Hacker có thể lợi dụng bằng cách gửi header giả mạo với độ dài khổng lồ (như 2GB) qua các kết nối mới, nhanh chóng tiêu hao tài nguyên máy chủ.
Ở phiên bản gate đầu tiên của skynet, vấn đề này được giải quyết bằng cơ chế ringbuffer chia sẻ có kích thước cố định. Tuy nhiên phiên bản hiện tại không còn hỗ trợ 4 byte nên cũng loại bỏ biện pháp đặc biệt này. Nếu ứng dụng của bạn thực sự cần độ dài gói tin vượt ngưỡng 64KB, hãy cẩn trọng xây dựng module phân gói riêng thay vì đơn giản thay đổi 2 byte thành 4 byte trong thư viện netpack.