CodeGym /Kurslar /C# SELF /Ikilik (binary) formatına daha dərindən və onun problemlə...

Ikilik (binary) formatına daha dərindən və onun problemlərinə baxış

C# SELF
Səviyyə , Dərs
Mövcuddur

1. Giriş

Qısaca: serializasiya olunmuş ikilik format obyektləri bayt sırasına çevirir, hansı ki onun strukturunu və qiymətlərini mümkün qədər sıx formada kodlayır. Təsəvvür edin ki, siz obyekti sadəcə sözlərlə təsvir etmirsiniz (məsələn JSON və ya XML-də olduğu kimi), əksinə hər biti elə yazırsınız, necə yaddaşda saxlanılır.

Mətndə formatında verilən məlumat — dostunuza rus dilində yazılan məktub kimidir (hər simvol insan üçün anlaşılır). İkilik format isə daha çox mors kodudur: hər nöqtə və tirə maksimum qısa formada yazılıb və "əl ilə" oxumaq mümkün deyil.

Sxema: formatların müqayisəsi

Format İnsana oxunur Fayl ölçüsü Sürət (yazma/oxuma) Uyğunluq
XML/JSON Bəli Böyük Yavaş Yaxşı
İkilik (binary) Xeyr Kiçik Çox sürətli Məhdud

.NET-də ikilik serializasiya necə işləyir?

.NET-ekosistemdə tarixi olaraq əsas alət ikilik serializasiya üçün BinaryFormatter sinfi olub. Amma platforma inkişaf etdikcə o təhlükəli sayıldı və .NET-dən 9 versiyasında çıxarıldı. İndi standart yollar daha çox fərqlidir: BinaryWriter/BinaryReader, və mürəkkəb obyektlər üçün üçüncü tərəf kitabxanaları (məsələn, protobuf-net).

Qısa tarixi ekskurs

BinaryFormatter [Serializable] ilə işarələnmiş istənilən sinfi götürüb baytlara çevirə bilirdi və deserializasiya zamanı obyektin strukturunu bərpa edirdi. Sehr kimi səslənir, amma bunun içində çox problem var (bundan sonra danışacağıq).

Müasir vasitələr

Primitiv tiplər və sadə strukturlar üçün BinaryWriterBinaryReader siniflərini istifadə etmək rahatdır. Mürəkkəb obyektlər üçün isə üçüncü tərəf kitabxanaları daha uyğundur (məsələn, protobuf-net, MessagePack-CSharp və s.).

2. BinaryWriter vasitəsilə primitivlərin serializasiyası

Gəlin təhsil tətbiqimizi bir az inkişaf etdirək. Məsələn, istifadəçi parametrlərini (istifadəçi adı, topladığı xallar, giriş vaxtı) ikilik fayla yazmaq istəyirik.

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)
{
    // Faylı yazmaq üçün açırıq
    using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
    using var writer = new BinaryWriter(stream);

    // Məlumatı hissə-hissə yazırıq. Əvvəl string, sonra int, sonra tarix
    writer.Write(profile.Name ?? string.Empty); // string
    writer.Write(profile.Score);                // tam ədəd
    writer.Write(profile.LoginTime.ToBinary()); // tarix "long" kimi çevrilir
}

İndi oxumaq nümunəsi:

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 };
}

Primitiv ikilik serializasiyanın xüsusiyyətləri

BinaryWriter ilə biz hər bir sahəni ayrı-ayrılıqda serializə edirik. Bu etibarlı və proqnozlaşdırılan üsuldur: əgər məlumat strukturu dəyişirsə, bunu kodda görürük.

3. Klassik ikilik serializasiyanın problemləri

İndi digər tərəfə baxaq. Niyə Microsoft BinaryFormatter-i bu qədər sərt qınayıb və istifadəni qadağan edib?

Sxemin kövrəkliyi (Schema Evolution Hell)

İkilik məlumatlar sinfin strukturuna sıx bağlı olur. Sinfi dəyişdiniz (sahəni adını dəyişdiniz, yeni əlavə etdiniz, köhnəni sildiniz) — köhnə ikilik fayllar oxunmaz olur. Sahələrin sırasını dəyişdinizsə — yenə problem yaranır.

İllustrasiya:

// Dünən
public class Profile
{
    public string Name;
    public int Score;
}

// Bu gün
public class Profile
{
    public string Name;
    public double Rating; // Yeni sahə əlavə olundu
    public int Score;
}

Köhnə faylı oxumaq ya səhvə, ya da "qarışıq" məlumatlara gətirib çıxaracaq. JSON və ya XML-dən fərqli olaraq, orada yox olan elementləri atmaq olur; ikilik format bu dəyişikliklərə uyğunlaşmır — o beton yol kimidir: bir az kənara çıxdın — və dərhal "velosiped düşür".

Deserializasiya zəiflikləri

BinaryFormatter-in ən böyük problemi — potensial təhlükəsizlik zəifliyidir. Əgər proqramınız şübhəli mənbədən gələn ikilik məlumatları deserializasiya edirsə (məsələn, internetdən istifadəçidən), hücumçu zərərli "obyekt" yerləşdirə bilər. Keçmişdə bu hətta uzaqdan arbitrary kodun icrasına səbəb olub.

Çarpaz platforma uyğunluğu və uyğunluq

İkilik serializator .NET-in daxili təqdimatına, istifadə olunan runtime versiyasına, kompilyatora və arxitekturaya (məsələn, x64/ARM) sıx bağlıdır. Əgər siz Windows-da serializasiya edib sonra Linux-da deserializasiya etməyə çalışırsınızsa — sürprizlər qaçılmazdır! Hətta .NET versiyaları arasında da uyğunsuzluqlar yarana bilər.

Diaqnostikanın çətinliyi

Mətin formatlarında problem yarananda faylı açıb məzmununa baxmaq və nə səhv olduğunu anlamaq mümkün olur. İkilik fayl isə yeddi möhürlü sirr kimidir. Görəcəyiniz şey — məna kəsb etməyən bayt axınıdır. Belə faylı "analiz etmək" həvəs tələb edir.

4. Mürəkkəb obyektlərin ikilik serializasiyası

Sitatlar (References)

BinaryFormatter obyektlər arasındakı əlaqələri yadda saxlaya bilirdi (məsələn, iki sahə eyni obyektə istinad edirsə), amma BinaryWriter və əksər üçüncü tərəf kitabxanalarında belə sehr yoxdur. Adətən serializasiya "bir obyekt içində digərini yerləşdirdim və ardıcıl yazdım" prinsipi ilə gedir.

Dövri (cyclic) istinadlar

Məsələn, "ana" obyektin Child sahəsi, "uşaq" obyektin isə Parent sahəsi ana-əlaqəsinə işarə edirsə, bu cür obyektləri serializasiya etmək ya səhvə gətirir, ya da sonsuz dövr yaradır.

Nümunə:

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

Bu obyekti "naiv" şəkildə serializasiya etməyə çalışmaq dövrləşməyə gətirib çıxarar.

5. İkilik serializasiya və daşınabilmə

Hər hansı ikilik format (xüsusən öz əlinizlə yazılmış) — "yalnız öz içəridə" formatıdır. Əgər siz başqa proqramlarla məlumat mübadiləsi etmək və ya məlumatları "əbədi" saxlamaq istəyirsinizsə — açıq standartları seçin: JSON, XML və ya ProtoBuf.

İkilik serializasiyanın nə vaxt məntiqi var?

  • Əgər məlumat yalnız bir tətbiqin sərhədləri daxilində yaşayır və "qısa müddətə" saxlanılır.
  • Əgər sürət və sıxlıq vacibdirsə (məsələn, böyük loglar və ya bir ekosistem daxilində servisler arası mübadilə üçün).
  • Əgər siz serializasiya və deserializasiyanın hər iki tərəfini qəti nəzarətdə saxlayırsınız.

Alternativlər: protobuf, MessagePack və s.

  • protobuf-net: Google Protocol Buffers-un .NET üçün portu, çarpaz platforma mübadiləsi və uyğunluq üçün uyğundur.
  • MessagePack-CSharp: .NET üçün sürətli MessagePack implementasiyası.

"Sadə" BinaryWriter-dən fərqli olaraq, bu kitabxanalar sxemalar tətbiq edir, formatın evolusiyasını dəstəkləyir, çarpaz platforma uyğunluğu və təhlükəsizliyi təmin edir. Başqa sistemlərlə müəyyən dərəcədə uyğunluq planlaşdırırsınızsa, onları istifadə edin.

6. "Əl ilə" ikilik serializasiya

Əgər sizə yenə də ikilik məlumatları yazmaq lazımdırsa (məsələn, performans vacibdirsə), BinaryWriter/BinaryReader-dən istifadə edin — və həmişə aydın şəkildə yazma qaydasını və tipini kodlayın.

Məsləhətlər:

  • Həmişə məlumatı eyni sırada yazın, necə oxumağı planlaşdırırsınızsa elə də.
  • Struktur dəyişdikdə format nömrəsini saxlayın və ya "magic header" (Magic Header) yazın.
  • String/array uzunluğunu məlumatların özündən əvvəl əlavə edin.
  • Fayl strukturunu sənədləşdirin: əks halda bir il sonra öz formatınızı anlamayacaqsınız.

Nümunə: versiyalaşdırma

// Format versiyasını əvvəlcə saxlayırıq
writer.Write((byte)1); // Versiya 1

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

/*
Bu format dəyişdikdə oxumaq üçün əlavə şərtlər əlavə etməyə imkan verir
*/

7. İkilik serializasiya ilə işləyərkən tipik səhvlər

Sahələri bir sıra ilə kodlayıb, oxuyarkən yerlərini dəyişdiniz. Nəticədə dəyərlər "sürüşür": string int kimi oxunur, int tarix kimi və s.

10 obyekti yazdınız, amma oxuyanda 11 oxumağa çalışırsınız. Axın pozulur: EndOfStream barədə istisna atılacaq.

Sinif strukturunu dəyişdiniz və köhnə ikilik faylları oxumaq mümkün deyil — bütün tarix itə bilər.

Vacib faylı oxuma zamanı istisnaları nəzərə almadınız — tətbiq diskdə ilk nasazlıqda (məsələn, EndOfStreamException) çökəcək.

Aydın format olmadan müxtəlif proqramlaşdırma dilləri arasında ikilik fayllarla mübadilə etməyə çalışırlar99% hallarda bu zəmanətli ağrı deməkdir.

Şəbəkədən tanımadığınız istifadəçilərdən alınan məlumatları deserializasiya edirlər — salam zəifliklər! Heç vaxt BinaryFormatter-dən istifadə etməyin; girişləri validasiya edin və təhlükəsiz formatlardan istifadə edin.

Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION