Thiết Kế Một Giao Thức Buffer Protocol Đơn Giản - nói dối e blog

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.person {
  .address {
    email 0 : string
    phone 1 : string
  }
  name 0 : string
  age 1 : integer
  marital 2 : boolean
  children 3 : *person  // Mảng các đối tượng person
  address 4 : address
}

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:

1
2
3
4
5
6
foobar 1 {
  request person
  response {
    ok 0 : boolean
  }
}

Mô tả giao thức bằng chính giao thức

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
.type {
  .field {
    name 0 : string
    type 1 : string
    id 2 : integer
    array 3 : boolean
  }
  name 0 : string
  fields 1 : *field
}
.protocol {
  name 0 : string
  id 1 : integer
  request 2 : string
  response 3 : string
}
.group {
  type 0 : *type
  protocol 1 : *protocol
}

Giao diện lập trình API

1
2
local tag, bytes = encode("foobar.request",
  { name = "alice", age = 13, marital = false })  

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:

  1. 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
  2. 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ế

1
2
3
4
5
6
7
person { name = "Alice" , age = 13, marital = false } :
03 00 01 00 (fn = 3, dn = 1)
00 00 00 00 (id = 0, ref = 0)
00 00 0E 00 (id = 1, value = 13)
00 00 01 00 (id = 2, value = false)
05 00 00 00 (sizeof "Alice")
41 6C 69 63 65 00 00 00 ("Alice" align by 4)

Nén dữ liệu 0

Dựa trên kỹ thuật của Cap’n Proto:

  1. Bổ sung 0 để độ dài dữ liệu chia hết cho 8
  2. Chia thành từng khối 8 byte
  3. Sử dụng 1 byte bit-map để chỉ định vị trí khác 0
  4. Các byte không phải 0 được xếp liền sau bit-map

Ví dụ:

1
2
Dữ liệu gốc: 08 00 00 00 03 00 02 00  19 00 00 00 aa 01 00 00
Nén thành:    51
0%