Truyền Tải UDP Đáng Tin Cậy - nói dối e blog

Truyền Tải UDP Đáng Tin Cậy

Bài viết này được chia thành ba phần chính: thứ nhất, phân tích các trường hợp nên sử dụng giao thức UDP thay vì TCP; thứ hai, thiết kế giao diện API cho một mô-đun truyền tải UDP đáng tin cậy; và cuối cùng là một bản triển khai đơn giản.

Khi nào nên ưu tiên UDP thay vì TCP?

Tôi luôn cho rằng việc xây dựng một giao thức truyền tải đáng tin cậy trên nền UDP (ví dụ như mô phỏng TCP trên UDP) là một ý tưởng không khả thi. TCP đã là một giao thức cực kỳ phức tạp, và gần như không thể tái tạo hiệu quả hơn. Nếu cố gắng xây dựng một giao thức UDP đáng tin cậy nhưng hiệu suất vượt trội TCP, kết quả thường chỉ khả quan trong một số tình huống cụ thể, hoặc tệ hơn là chiếm dụng quá nhiều tài nguyên mạng. TCP được thiết kế với tiêu chí tối ưu hóa lưu lượng toàn mạng, trong khi các giao thức “ích kỷ” cố gắng chiếm băng thông sẽ dẫn đến nghẽn mạng toàn hệ thống.

Trong ngành công nghiệp game trực tuyến, đặc biệt là game di động, nhiều nhà phát triển luôn tìm cách tận dụng UDP để giảm độ trễ, đồng thời mong muốn độ tin cậy như TCP. Câu hỏi đặt ra là: Làm thế nào để cân bằng giữa tốc độ và độ tin cậy?

Nếu muốn UDP vượt trội TCP, chúng ta phải hy sinh một số tính năng của TCP. Một hướng tiếp cận là chấp nhận việc mất gói tin trong các trường hợp nhất định. Ví dụ, khi đồng bộ trạng thái có thời hạn hiệu lực, thông tin trạng thái cũ có thể được bỏ qua nếu đã quá hạn. Tuy nhiên, việc này khiến tầng ứng dụng trở nên phức tạp, vì không phải lúc nào cũng có thể đồng bộ trạng thái toàn phần.

Một hướng khác là cho phép gói tin đến không theo thứ tự, nhưng đảm bảo tất cả đều được giao. Bằng cách thêm số thứ tự vào mỗi gói (giống TCP), chúng ta có thể xử lý gói tin đến muộn mà không làm gián đoạn luồng dữ liệu. Tuy nhiên, trường hợp này chỉ phù hợp với các giao dịch “một hỏi - một đáp” như truy vấn DNS, nơi thứ tự gói tin không quan trọng.

Trong môi trường mạng kém ổn định, việc sử dụng kết nối ngắn (short-lived connection) thay vì kết nối dài (long-lived connection) có thể cải thiện trải nghiệm người dùng. Ví dụ, giao thức QUIC của Google tối ưu hóa HTTP trên UDP bằng cách giảm số lần bắt tay so với TCP truyền thống.

Thiết kế API cho mô-đun UDP đáng tin cậy

Dựa trên kinh nghiệm phân tích các thư viện mã nguồn mở, tôi nhận thấy một điểm yếu phổ biến là mô-đun truyền tải bị phụ thuộc chặt chẽ vào lớp UDP (ví dụ: tự quản lý việc gửi/nhận gói tin UDP). Điều này gây khó khăn khi tích hợp vào kiến trúc mạng hiện có.

Thay vào đó, mô-đun nên chỉ tập trung vào việc xử lý dữ liệu đầu vào/đầu ra, tách biệt hoàn toàn với lớp giao tiếp mạng. Giao diện API có thể được thiết kế như sau:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct rudp_package {
   struct rudp_package *next;
   char *buffer;
   int sz;
};
struct rudp * rudp_new(int send_delay, int expired_time);
void rudp_delete(struct rudp *);
int rudp_recv(struct rudp *U, char buffer[MAX_PACKAGE]);
void rudp_send(struct rudp *U, const char *buffer, int sz);
struct rudp_package * rudp_update(struct rudp *U, const void * buffer, int sz, int tick);

Hàm rudp_update đóng vai trò trung tâm, yêu cầu tầng ứng dụng gọi định kỳ theo thời gian thực. Tham số tick giúp phân biệt các lần gọi trong cùng một khoảng thời gian. Khi tick > 0, mô-đun sẽ xử lý dữ liệu tích lũy từ các lần gọi trước.

Triển khai chi tiết

Phiên bản cuối cùng của tôi bao gồm các đặc điểm sau:

  1. Đánh số thứ tự 16-bit: Các gói tin được đánh số từ 0 đến 65535, sau đó quay vòng. Nếu nhận gói tin có số thứ tự chênh lệch ±32768 so với gói trước, mô-đun sẽ tự động điều chỉnh để tránh nhầm lẫn.
  2. Gói vật lý và gói logic: Nhiều gói logic có thể được đóng gói trong một gói vật lý, nhưng kích thước gói vật lý không vượt quá 512 byte.
  3. Cơ chế yêu cầu lại: Thay vì xác nhận (ACK) như TCP, bên nhận có thể yêu cầu bên gửi truyền lại gói tin bị thiếu.
  4. Các loại gói tin cố định:
    • Gói tim đập (Heartbeat)
    • Gói báo lỗi kết nối
    • Gói yêu cầu lại (+2 byte số thứ tự)
    • Gói thông báo lỗi (+2 byte số thứ tự)

Mã nguồn triển khai đơn giản (khoảng 500 dòng C) đã được công bố trên GitHub:

Kết luận

Việc xây dựng một giao thức UDP đáng tin cậy chỉ nên áp dụng khi:

  • Dữ liệu truyền tải có kích thước nhỏ (dưới 500 byte).
  • Ứng dụng cần độ trễ thấp và chấp nhận một số ràng buộc về thứ tự gói tin.
  • Cơ chế yêu cầu lại và tim đập được thiết kế tối ưu để giảm thiểu tài nguyên mạng.

Mặc dù không thể thay thế hoàn toàn TCP, nhưng giải pháp này có thể mang lại hiệu suất vượt trội trong các ứng dụng thời gian thực như game hoặc IoT.

0%