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 |

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:

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/cbornén số nguyên gọn theo mặc định (byte1a= 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:
CBORMarshal415,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/cborlà 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ề
- 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).
- 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.
- 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
- IETF — RFC 8949: Concise Binary Object Representation (CBOR): https://www.rfc-editor.org/rfc/rfc8949.html
- Go — fxamacker/cbor: https://pkg.go.dev/github.com/fxamacker/cbor/v2
- Trang chủ CBOR (danh sách tag, ứng dụng): https://cbor.io/
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ị.