Thiết Kế Một Giao Thức Buffer Protocol Đơn Giản
Tôi đang phát triển một giao thức đệm dữ liệu đơn giản hóa dựa trên trải nghiệm sử dụng Protocol Buffer của Google trong các dự án 3 năm qua. Thư viện pbc mà tôi từng xây dựng đã trải qua nhiều thử nghiệm thực tế, nhưng tôi nhận ra rằng phần lớn tính năng phức tạp của giao thức truyền thống đều không cần thiết cho nhu cầu của chúng tôi.
Triết lý thiết kế
Tôi nhận thấy việc xây dựng một giao thức RPC riêng biệt trên nền Protocol Buffer truyền thống lại tạo ra độ phức tạp cao hơn cả việc thiết kế một giao thức hoàn toàn mới. Đặc biệt trong môi trường Lua mà chúng tôi sử dụng chủ yếu, việc tùy biến linh hoạt hoàn toàn khả thi.
Kiểu dữ liệu cốt lõi
Chúng tôi chỉ cần 4 kiểu dữ liệu cơ bản:
- boolean: Giá trị đúng/sai
- integer: Số nguyên 32-bit có dấu
- string: Chuỗi ký tự
- id: Số nguyên 64-bit không dấu Cùng hai cấu trúc người dùng:
- array: Mảng các phần tử cùng kiểu
- struct: Cấu trúc dữ liệu tổ hợp
Giải thích các quyết định thiết kế
Tại sao không có kiểu float?
Trong suốt các dự án vừa qua, việc sử dụng số thực cực kỳ hạn chế. Khi cần thiết, chúng tôi hoàn toàn có thể dùng chuỗi để truyền giá trị float.
Không cần enum
Việc chuyển đổi giữa số nguyên và enum hoàn toàn có thể xử lý ở tầng nghiệp vụ, không cần tích hợp vào giao thức truyền thông.
Không dùng union
Việc đánh dấu các trường bằng tag số nguyên đã đủ để xác định dữ liệu cần truyền, không cần cấu trúc union phức tạp. Các trường không cần truyền sẽ đơn giản bị bỏ qua trong quá trình đóng gói.
Không dùng giá trị mặc định
Trải nghiệm từ thư viện pbc cho thấy việc dựa vào giá trị mặc định thường dẫn đến hiểu lầm. Thay vào đó, việc không đóng gói một trường sẽ tương đương với giá trị mặc định - một quy ước rõ ràng hơn nhiều so với mô hình của Protocol Buffer gốc.
Cú pháp mô tả giao thức
Tôi đặt tên cho giao thức mới là ejoyproto với cú pháp mô tả dễ đọc như sau:
|
|
Quy tắc đặt tên
- Tuân theo quy tắc C: chỉ chứa chữ cái, số và dấu gạch dưới
- Không bắt đầu bằng số
- Phân biệt chữ hoa/chữ thường
- Tên kiểu tự định nghĩa phải bắt đầu bằng dấu chấm (.)
Mô tả RPC
Giao thức RPC được tích hợp trực tiếp:
|
|
Mô tả giao thức bằng chính giao thức
|
|
Giao diện lập trình API
|
|
Cách mã hóa dữ liệu (Wire Protocol)
Tất cả số nguyên đều được mã hóa theo định dạng little-endian. Mỗi gói tin chia làm hai phần:
- Thông tin trường dữ liệu: Các trường được sắp xếp theo thứ tự tăng dần của tag
- Khối dữ liệu: Chứa các chuỗi byte cần thiết, được căn chỉnh theo độ dài 4 byte
Cấu trúc gói tin
- Mỗi trường chiếm 2 word (4 byte):
- Word 1: Hiệu số tag so với trường trước (trừ 1)
- Word 2: Giá trị trường, nếu khác 0 thì là giá trị thực (trừ 1), nếu bằng 0 thì trỏ đến khối dữ liệu
Quy tắc xử lý dữ liệu
- integer: Khối dữ liệu dài 4 byte
- id: Khối dữ liệu dài 8 byte
- string: Độ dài khối bằng độ dài chuỗi
- Kiểu người dùng: Khối dữ liệu chứa toàn bộ cấu trúc
- Mảng:
- Mảng số nguyên: mỗi phần tử 4 byte
- Mảng boolean: mỗi byte chứa 8 giá trị (từ LSB đến MSB)
- Mảng chuỗi/cấu trúc: độ dài + nội dung
Ví dụ thực tế
|
|
Nén dữ liệu 0
Dựa trên kỹ thuật của Cap’n Proto:
- Bổ sung 0 để độ dài dữ liệu chia hết cho 8
- Chia thành từng khối 8 byte
- Sử dụng 1 byte bit-map để chỉ định vị trí khác 0
- Các byte không phải 0 được xếp liền sau bit-map
Ví dụ:
|
|