CodeGym /Các khóa học /C# SELF /Sâu hơn về định dạng nhị phân và các vấn đề của nó

Sâu hơn về định dạng nhị phân và các vấn đề của nó

C# SELF
Mức độ, Bài học
Có sẵn

1. Giới thiệu

Nói ngắn gọn: định dạng nhị phân của serialization biến một object thành một dãy byte, mã hoá cấu trúc và giá trị của nó tối ưu nhất về kích thước. Hãy tưởng tượng bạn không chỉ mô tả object bằng từ ngữ (như JSON hoặc XML), mà ghi từng bit của nó đúng như lưu trong bộ nhớ.

Trong định dạng văn bản dữ liệu giống như một bức thư gửi bạn bè bằng tiếng Nga (mỗi ký tự dễ hiểu với con người). Trong định dạng nhị phân thì giống mã Morse, mỗi dấu chấm và gạch được ghi ngắn nhất có thể, và đọc "bằng tay" gần như không khả thi.

Sơ đồ: so sánh các định dạng

Định dạng Dễ đọc với con người Kích thước file Tốc độ (ghi/đọc) Tương thích
XML/JSON Có Lớn Chậm hơn Tốt
Nhị phân Không Nhỏ Rất nhanh Bị giới hạn

Định dạng nhị phân hoạt động thế nào trong .NET?

Trong hệ sinh thái .NET truyền thống công cụ chính cho serialization nhị phân là lớp BinaryFormatter. Nhưng theo thời gian nó bị coi là không an toàn và bị loại khỏi .NET 9. Hiện giờ tiêu chuẩn là các cách khác: BinaryWriter/BinaryReader, và với object phức tạp — thư viện bên thứ ba (ví dụ protobuf-net).

Tóm tắt về cái cũ (bối cảnh lịch sử)

BinaryFormatter có thể lấy bất kỳ lớp nào được gắn attribute [Serializable] và biến nó thành byte, khi deserialize có thể khôi phục cấu trúc object. Nghe có vẻ kỳ diệu, nhưng điều đó ẩn chứa nhiều vấn đề (sẽ nói bên dưới).

Các công cụ hiện đại

Với các kiểu nguyên thủy và cấu trúc đơn giản tiện dùng các lớp BinaryWriter và BinaryReader. Với object phức tạp — dùng thư viện bên thứ ba (ví dụ protobuf-net, MessagePack-CSharp, v.v.).

2. Serialization kiểu nguyên thủy bằng BinaryWriter

Tiếp tục hoàn thiện ứng dụng học của chúng ta. Ví dụ, ta muốn lưu cài đặt người dùng (tên người dùng, số điểm, thời gian đăng nhập) vào file nhị phân.

public class UserProfile
{
    public string Name { get; set; }
    public int Score { get; set; }
    public DateTime LoginTime { get; set; }
}

public static Task SaveUserProfile(UserProfile profile, string filePath)
{
    // Mở file để ghi
    using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
    using var writer = new BinaryWriter(stream);

    // Ghi dữ liệu từng phần. Trước tiên là chuỗi, rồi số, rồi ngày
    writer.Write(profile.Name ?? string.Empty); // chuỗi
    writer.Write(profile.Score);                // số nguyên
    writer.Write(profile.LoginTime.ToBinary()); // ngày được chuyển thành "long"
}

Bây giờ ví dụ đọc:

public static Task<UserProfile> LoadUserProfile(string filePath)
{
    using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
    using var reader = new BinaryReader(stream);

    string name = reader.ReadString();
    int score = reader.ReadInt32();
    long dateData = reader.ReadInt64();
    DateTime loginTime = DateTime.FromBinary(dateData);

    return new UserProfile { Name = name, Score = score, LoginTime = loginTime };
}

Đặc điểm của serialization nhị phân nguyên thủy

Với BinaryWriter ta serialize từng property riêng lẻ. Đây là cách đáng tin cậy và dự đoán được: nếu cấu trúc dữ liệu thay đổi, ta sẽ thấy rõ trong code.

3. Vấn đề của serialization nhị phân cổ điển

Giờ xem mặt trái của vấn đề. Tại sao Microsoft phản đối mạnh mẽ BinaryFormatter và thậm chí cấm dùng?

Mong manh của format (Schema Evolution Hell)

Dữ liệu nhị phân bị ràng buộc chặt với cấu trúc lớp. Thay đổi lớp (đổi tên field, thêm mới, xoá trường) — file nhị phân cũ có thể không đọc được. Thay đổi thứ tự property — cũng là vấn đề.

Minh hoạ:

// Hôm qua
public class Profile
{
    public string Name;
    public int Score;
}

// Hôm nay
public class Profile
{
    public string Name;
    public double Rating; // Thêm field mới
    public int Score;
}

Đọc file cũ có thể gây lỗi, hoặc đọc ra dữ liệu "trộn lẫn". Không như JSON hay XML, nơi phần thiếu có thể bỏ qua, định dạng nhị phân không thích nghi với thay đổi — nó như một con đường bê tông cứng: lệch một chút là "ngã xe".

Lỗ hổng khi deserialize

Vấn đề lớn nhất của BinaryFormatter là tiềm ẩn lỗ hổng. Nếu phần mềm bạn deserialize dữ liệu nhị phân từ nguồn không tin cậy (ví dụ người dùng trên mạng), kẻ tấn công có thể đưa vào object độc hại. Trước đây điều này từng dẫn tới thực thi mã từ xa trên máy nạn nhân.

Tính đa nền tảng và tương thích

Serializer nhị phân gắn chặt vào cách biểu diễn nội bộ của .NET và phiên bản runtime, compiler, kiến trúc (ví dụ x64/ARM). Nếu serialize trên Windows và deserialize trên Linux — sẽ có bất ngờ! Thậm chí giữa các phiên bản .NET có thể không tương thích.

Khó khăn chẩn đoán

Khi có vấn đề với định dạng văn bản bạn có thể mở file, xem nội dung và đoán lỗi. File nhị phân là một bí ẩn. Những gì mắt bạn thấy chỉ là một luồng byte vô nghĩa. "Phân tích" file kiểu này là thú chơi cho người thích mày mò.

4. Serialization nhị phân cho object phức tạp

Tham chiếu

BinaryFormatter có thể nhớ các liên kết giữa object (ví dụ hai property tham chiếu cùng một object), nhưng BinaryWriter và hầu hết thư viện bên thứ ba không có phép màu đó. Thường serialization làm theo nguyên tắc "ghi object lồng nhau lần lượt".

Tham chiếu vòng

Serialize object có tham chiếu vòng (ví dụ "mẹ" có property Child, và "con" có property Parent trỏ về cha) sẽ gây lỗi hoặc vòng lặp vô hạn.

Ví dụ:

public class Node
{
    public Node? Next { get; set; }
    public Node? Prev { get; set; }
}

Cố gắng serialize object này theo cách "ngây thơ" sẽ dẫn tới vòng lặp.

5. Serialization nhị phân và khả năng chuyển giao

Bất kỳ định dạng nhị phân nào (đặc biệt tự viết) đều là "chỉ cho nội bộ". Nếu bạn muốn trao đổi dữ liệu với chương trình khác hoặc lưu trữ "lâu dài" — chọn tiêu chuẩn mở: JSON, XML hoặc ProtoBuf.

Khi nào nên dùng serialization nhị phân?

  • Nếu dữ liệu chỉ sống trong một ứng dụng và lưu "ngắn hạn".
  • Nếu cần tốc độ và gọn (ví dụ logs lớn hoặc trao đổi giữa service trong cùng hệ sinh thái).
  • Nếu bạn kiểm soát chặt cả hai phía: serialize và deserialize.

Alternatives: protobuf, MessagePack và các thư viện khác

  • protobuf-net: port của Google Protocol Buffers cho .NET, phù hợp trao đổi cross-platform và tương thích.
  • MessagePack-CSharp: implement nhanh của MessagePack cho .NET.

Không như "BinaryWriter thô", các thư viện này cung cấp schema, hỗ trợ evolution của format, cross-platform và an toàn. Dùng chúng nếu bạn muốn tương thích với hệ thống khác.

6. "Thủ công" serialization nhị phân

Nếu bạn vẫn cần ghi nhị phân (ví dụ ứng dụng chú trọng performance), dùng BinaryWriter/BinaryReader — và luôn mã hoá rõ thứ tự và kiểu dữ liệu.

Mẹo:

  • Luôn ghi dữ liệu theo cùng thứ tự bạn dự định đọc.
  • Khi thay đổi cấu trúc file thì giữ số version hoặc viết "magic header" (Magic Header).
  • Ghi độ dài chuỗi/array trước khi ghi dữ liệu thực tế.
  • Tài liệu hoá cấu trúc file: nếu không năm sau bạn sẽ không hiểu định dạng của mình.

Ví dụ: versioning

// Ghi số phiên bản định dạng ở vị trí đầu
writer.Write((byte)1); // Phiên bản 1

writer.Write(profile.Name ?? "");
writer.Write(profile.Score);
writer.Write(profile.LoginTime.ToBinary());

/*
Cho phép khi thay đổi định dạng có thể thêm điều kiện đọc
*/

7. Lỗi điển hình khi làm việc với serialization nhị phân

Ghi các field theo một thứ tự, khi đọc lại đổi chỗ. Hậu quả là giá trị "dịch lệch": chuỗi được đọc như int, int — như ngày, v.v.

Ghi 10 object nhưng đọc 11. Dòng luồng bị phá vỡ: sẽ có exception báo EOF.

Thay đổi cấu trúc lớp, nhưng không đọc được file nhị phân cũ — mất hết lịch sử dữ liệu.

Quên xử lý exception khi đọc file quan trọng — app sập khi gặp lỗi ổ đĩa (ví dụ EndOfStreamException).

Cố gắng trao đổi file nhị phân giữa ngôn ngữ khác nhau mà không có định dạng rõ ràng — trong 99% trường hợp đó là cơn đau đã được đảm bảo.

Deserialize dữ liệu nhận từ mạng từ user lạ — chào mừng lỗ hổng! Không bao giờ dùng BinaryFormatter; validate input và dùng định dạng an toàn.

2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Ghi dữ liệu người dùng vào file nhị phân
Ghi dữ liệu người dùng vào file nhị phân
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION