Giao Thức Gói Mạng Có Vẻ Hợp Lý - nói dối e blog

Giao Thức Gói Mạng Có Vẻ Hợp Lý

Giao thức gói tin hợp lý đến mức lừa đảo

Thời gian gần đây tôi bắt đầu một dự án nhỏ gọn, phát triển nhanh chóng và hoàn thiện cũng chẳng mấy chốc. Đó là một tựa game 2D với bộ engine đồ họa đã được kiểm chứng, cùng với ngọn lửa đam mê Lua đang rực cháy, tôi quyết định bao bọc toàn bộ engine C++ gốc bằng Lua. Kết quả thật đáng ngạc nhiên - không chỉ hiệu suất vận hành mượt mà, mà cả phần giao diện người dùng cũng được đóng gói rất tiện lợi. Việc sử dụng Lua để điều khiển logic trò chơi dường như đã mở ra một chân trời mới. Có lúc tôi còn mơ tưởng đến viễn cảnh open-source dự án này, biết đâu lại trở thành chuẩn mực cho phát triển game 2D trên Lua. Dù rõ ràng ý nghĩ ấy hơi viển vông, nhưng cứ để nó tồn tại như một khát vọng đẹp.

Dự án nhỏ như thế này rất thích hợp để rèn luyện kỹ năng đội nhóm. Với vai trò là người xây dựng nền tảng cơ bản, tôi tranh thủ lấy nó làm bài giảng cho đồng nghiệp mới. Đứng nhìn người khác code mà lòng ngứa ngáy khó chịu, càng thấy bứt rứt khi họ gặp khó khăn, cứ muốn xắn tay vào làm thay mọi việc.

Trong số các nhiệm vụ được phân công, đồng nghiệp phụ trách thiết kế lớp bao bọc giao thức mạng đang nỗ lực tối đa để tạo ra một giải pháp dễ sử dụng bằng Lua. Thật lòng mà nói, kinh nghiệm thực chiến của tôi với lập trình mạng còn rất hạn chế - chưa tới 10 nghìn dòng code, và sản phẩm hoàn chỉnh đếm cũng chẳng quá 5 cái. Một phần trong số đó dùng UDP, nhưng lần này chúng tôi chọn TCP làm giao thức chính. Ký ức ùa về những ngày xưa cũ với giao thức truyền thông “Đại Thoại”, trong đó byte đầu tiên là type, theo sau là 2 byte độ dài gói tin.

Khi đó tôi từng phàn nàn về thiết kế này. Vì TCP là giao thức luồng, nếu cứ dùng độ dài gói tin để phân mảnh dữ liệu rồi mới dựa vào type để điều hướng logic, lẽ ra nên đặt thông tin độ dài lên đầu tiên mới phải. Như thế, mã底层 (lower-level) mới có thể tách luồng dữ liệu thành các gói hoàn chỉnh trước, sau đó mới chuyển toàn bộ gói tin cho tầng logic không liên quan đến mạng xử lý.

Lần này, khi thiết kế giao thức, tôi quyết định dùng script Lua để mô tả từng giao thức cụ thể. Phần C++ sẽ đảm nhiệm việc phân tích cú pháp luồng mạng thành bảng (table) Lua, giúp phía Lua xử lý dữ liệu mạng trở nên thuận tiện hơn bao giờ hết.

Ở chế độ không chặn (non-blocking), khi dùng API socket xử lý TCP, chỉ có đọc từng byte đơn lẻ mới được xem là thao tác nguyên tử. Điều đó có nghĩa là khi gói tin dài hơn một byte, không thể mặc định đọc một lần là xong. Giải pháp thông thường là xây dựng một máy trạng thái (state machine) để xử lý luồng dữ liệu. Cấu trúc đơn giản “độ dài + kiểu + nội dung” có thể chia thành 3 trạng thái tương ứng. Trong vòng lặp chính, chỉ cần liên tục gọi xử lý máy trạng thái là hoàn thành nhiệm vụ nhận dữ liệu mạng.

Tuy nhiên, khi thử nghiệm theo tư tưởng này, tôi chợt nhận ra độ dài gói tin hóa ra lại là thông tin thừa hoàn toàn. Bởi lẽ nếu đã dùng script để định nghĩa cấu trúc từng giao thức, độ dài gói hoàn toàn có thể suy luận được từ type. Đa số giao thức gồm các thành phần cố định, tổng thể cũng mang độ dài cố định. Một số ít có phần tử biến đổi như chuỗi ký tự hay mảng, nhưng độ dài tổng vẫn có thể xác định theo ngữ cảnh. Việc xử lý các trường hợp này chẳng qua chỉ làm phức tạp thêm chút ít cho máy trạng thái mà thôi.

Vì vậy, tôi quyết định loại bỏ hoàn toàn thông tin độ dài khỏi phần đầu gói tin.

0%