Phần trước ta thấy MessagePack là "JSON nhị phân". Vậy tại sao IETF — tổ chức chuẩn hóa Internet — lại tạo thêm CBOR (Concise Binary Object Representation, RFC 8949), một định dạng gần như giống hệt MessagePack? Câu trả lời không nằm ở kích thước hay tốc độ (chúng xấp xỉ nhau vì cùng ý tưởng), mà ở sự chuẩn hóa và tag ngữ nghĩa. CBOR là nền tảng của những thứ bạn dùng hằng ngày mà không biết: WebAuthn (đăng nhập bằng vân tay/khóa bảo mật), COSE (ký/mã hóa), CWT (token kiểu JWT nhưng nhị phân), và vô số thiết bị IoT. Bài này (phần 7 loạt Serialization) chạy thật để đặt CBOR cạnh MessagePack và JSON.

Cơ chế: major type ở 3 bit cao

Dùng CBOR trong Go y hệt các định dạng trước:

c, _ := cbor.Marshal(order)   // struct -> []byte CBOR
var o Order
cbor.Unmarshal(c, &o)         // []byte -> struct

Cấu trúc byte của CBOR rất sạch: byte đầu tiên chia làm hai phần — 3 bit cao là major type (kiểu chính), 5 bit thấp là additional info (thông tin phụ, thường là độ dài hoặc giá trị nhỏ):

byte đầu = (major_type << 5) | additional_info
major type ý nghĩa major type ý nghĩa
0 unsigned int 4 array
1 negative int 5 map
2 byte string 6 tag (ngữ nghĩa)
3 text string 7 simple/float/bool/nil

Ảnh chụp đoạn mã Go nền tối minh hoạ CBOR RFC 8949, dùng y như JSON msgpack c bằng cbor Marshal order struct sang byte CBOR var o Order cbor Unmarshal c o byte sang struct, cơ chế major type ở 3 bit cao của byte đầu byte đầu bằng major_type dịch 5 hoặc additional_info major type 0 unsigned int 1 negative int 2 byte string 3 text string 4 array 5 map 6 tag ngữ nghĩa 7 simple float bool nil 0xa7 bằng 101 00111 major 5 map 7 field 0x62 bằng 011 00010 major 3 text dài 2 bằng id, điểm riêng của CBOR TAG ngữ nghĩa major type 6 tag gắn nghĩa cho giá trị theo sau vài tag chuẩn tag 0 chuỗi thời gian RFC3339 tag 1 epoch số tag 2 bignum không dấu tag 32 URI thời gian mã hóa thành 1 số epoch 5 byte thay vì chuỗi 21 byte CBOR là chuẩn IETF dùng trong COSE WebAuthn CWT IoT

Hình 1: CBOR mã hóa như JSON/msgpack; byte đầu ghép major type (3 bit cao) với additional info — 0xa7 là map 7 field, 0x62 là text "id"; điểm riêng là tag (major type 6) gắn ngữ nghĩa cho giá trị, ví dụ thời gian thành epoch.

Đo thật: CBOR cạnh MessagePack và JSON

Mình mã hóa cùng đơn hàng (thêm cả trường thời gian) bằng cả ba, đo kích thước và benchmark:

Ảnh chụp bảng kết quả chạy thật CBOR output thật, một kích thước cùng dữ liệu 1 record JSON 440 MessagePack 392 CBOR 339 byte N bằng 100 JSON 44000 MessagePack 39200 CBOR 33900 CBOR nhỏ hơn JSON 1.30x nhỏ hơn cả msgpack trừ 53 byte fxamacker cbor nén số nguyên gọn mặc định msgpack thì không, hai byte format cộng TAG ngữ nghĩa a7 62 69 64 1a 00 01 5f a7 major 5 map 7 field 62 69 64 major 3 text id 1a major 0 uint 4 byte theo sau bằng 90000 TAG thời gian epoch tag 1 bằng 5 byte vs RFC3339 text bằng 21 byte tag ngữ nghĩa giúp mã hóa thời gian gọn hơn hẳn chuỗi, ba benchmark JSONMarshal 623.6 ns op 640 B op 3 allocs op MsgpackMarshal 717.7 ns op 1152 B op 6 allocs op CBORMarshal 415.6 ns op 496 B op 2 allocs op nhanh nhất JSONUnmarshal 2935 ns op 968 B op 21 allocs op CBORUnmarshal 1260 ns op 528 B op 14 allocs op 2.3x JSON, kết luận kích thước và tốc độ CBOR xấp xỉ nhỉnh hơn MessagePack cùng ý tưởng khác biệt lớn nhất CBOR là chuẩn IETF RFC 8949 cộng tag ngữ nghĩa nên chọn CBOR khi cần chuẩn liên thông WebAuthn COSE CWT IoT

Hình 2: Chạy thật — kích thước CBOR 339 byte (nhỏ hơn MessagePack 392 và JSON 440); byte a7=map 7 field, 1a=uint 4-byte; tag thời gian epoch chỉ 5 byte vs RFC3339 21 byte; benchmark CBOR marshal 415,6 ns/2 allocs (nhanh nhất), unmarshal 1.260 ns (~2,3x JSON).

Đọc kết quả đo được:

  • CBOR nhỏ nhất trong ba: 339 byte so với MessagePack 392 và JSON 440 — nhỏ hơn JSON 1,30 lần và nhỏ hơn cả MessagePack 53 byte. Lý do: thư viện fxamacker/cbor nén số nguyên gọn theo mặc định (byte 1a = uint với 4 byte theo sau cho 90000), trong khi thư viện MessagePack mặc định dùng int64 cố định 8 byte (như phần 6 đã thấy). Đây là điểm quan trọng: chất lượng thư viện ảnh hưởng kết quả nhiều như bản thân định dạng.
  • Mã hóa nhanh nhất: CBORMarshal 415,6 ns với 2 allocs — nhanh hơn cả JSON (623,6 ns) lẫn MessagePack (717,7 ns), và ít cấp phát nhất. fxamacker/cbor là một thư viện được tối ưu rất tốt. Giải mã cũng nhanh ~2,3 lần JSON (1.260 vs 2.935 ns).
  • Tag ngữ nghĩa — điểm riêng của CBOR: đây là thứ MessagePack không có sẵn dạng chuẩn. Mã hóa cùng một thời điểm, dùng tag 1 (epoch) cho ra 5 byte, còn chuỗi RFC3339 tốn 21 byte. Tag (major type 6) gắn ý nghĩa cho giá trị theo sau: tag 0 = thời gian dạng chuỗi, tag 1 = epoch, tag 2 = số lớn, tag 32 = URI... Bên giải mã biết cách diễn giải đúng mà không cần thỏa thuận ngoài.

Đánh đổi cần cân nhắc

Chọn CBOR thay MessagePack chủ yếu vì chuẩn hóa, không phải hiệu năng. Về kích thước và tốc độ, hai định dạng gần như hòa (chênh lệch ở đây đến từ thư viện, không phải bản chất). Lý do thật để chọn CBOR là nó là chuẩn IETF với đặc tả rõ ràng, có tag ngữ nghĩa, và một hệ sinh thái chuẩn xây trên nó (COSE để ký/mã hóa, CWT cho token, WebAuthn cho xác thực). Nếu bạn làm việc trong những lĩnh vực đó — bảo mật, IoT, thiết bị hạn chế tài nguyên — CBOR gần như là mặc định. Cho trao đổi nội bộ đơn thuần, MessagePack cũng tốt tương đương.

Vẫn lưu tên field như JSON/MessagePack. Giống hai họ hàng tự mô tả, CBOR mã hóa map với tên field ("id", "ma"...) trong dữ liệu. Nên dù nhỏ hơn JSON, nó vẫn không đạt độ gọn của Protobuf (~2 lần) vốn bỏ hẳn tên field. CBOR có cách dùng số nguyên làm khóa map để tiết kiệm (như COSE làm), nhưng khi đó bạn mất tính tự mô tả bằng tên — lại tiến gần mô hình cần schema.

"Concise" nhưng có nhiều chế độ; cẩn thận tính xác định. CBOR cho phép nhiều cách mã hóa cùng một giá trị (ví dụ số 0 có thể ở vài dạng), nên hai bộ mã hóa có thể ra byte khác nhau cho cùng dữ liệu. Với chữ ký số (COSE) điều này chết người — cùng nội dung mà byte khác thì chữ ký sai. Vì vậy có CBOR xác định (deterministic/canonical) với quy tắc chọn dạng ngắn nhất. Khi CBOR dùng cho ký hoặc so sánh byte, phải bật chế độ xác định.

Ba ý mang về

  1. CBOR là MessagePack được chuẩn hóa (RFC 8949): cùng ý tưởng byte format (major type 3 bit + additional info), nên kích thước và tốc độ xấp xỉ nhau — đo thật CBOR 339 byte, mã hóa 415 ns/2 allocs (nhanh nhất trong ba nhờ thư viện tốt).
  2. Tag ngữ nghĩa là điểm riêng đáng giá: đo thật thời gian mã hóa bằng tag epoch chỉ 5 byte so với chuỗi RFC3339 21 byte; tag gắn ý nghĩa (thời gian, số lớn, URI...) mà bên nhận hiểu đúng không cần thỏa thuận ngoài.
  3. Chọn CBOR vì chuẩn hóa và hệ sinh thái, không vì hiệu năng vượt trội: nó là nền tảng của WebAuthn, COSE, CWT, IoT — nếu làm bảo mật/thiết bị hạn chế thì CBOR gần như mặc định; nhớ bật chế độ xác định khi dùng cho chữ ký.

Nguồn

Phần sau ta zoom vào một kỹ thuật nền tảng của mọi định dạng nhị phân trên: varint và ZigZag — cách mã hóa số nguyên để số nhỏ tốn ít byte, và vì sao số âm cần ZigZag; đo thật số byte theo độ lớn giá trị.