Domanda

Sto usando la serializzazione binaria (BinaryFormatter) come meccanismo temporaneo per memorizzare le informazioni di stato in un file per un tempo relativamente complesso (gioco) struttura dell'oggetto; i file stanno venendo fuori molto grande di quanto mi aspettavo, e la mia struttura dati include riferimenti ricorsivi - quindi mi chiedo se il BinaryFormatter è in realtà la memorizzazione di più copie degli stessi oggetti, o se il mio numero di base" di oggetti e valori devo avere" arithmentic è lontano-base, o dove altro al sovradimensionamento proviene.

Ricerca su Stack Overflow sono stato in grado di trovare le specifiche per il formato binario la comunicazione remota di Microsoft: http://msdn.microsoft.com/en-us/ biblioteca / cc236844 (PROT.10) aspx

Quello che non riesco a trovare è un qualsiasi spettatore esistente che vi permette di "sbirciare" nel contenuto di un file di output BinaryFormatter - ottenere la conta degli oggetti e byte totali per i diversi tipi di oggetti nel file, ecc;

Mi sento come se questo deve essere il mio "google-fu" mi ha mancato (quel poco che ho) - chiunque aiutare lattina? Questo deve è stato fatto prima, giusto ??


Aggiorna : non riuscivo a trovarlo e non ho ricevuto risposte così ho messo qualcosa di relativamente veloce insieme (link al progetto scaricabile qui sotto); Posso confermare la BinaryFormatter non memorizzare più copie dello stesso oggetto, ma lo fa stampare un bel po 'di metadati al flusso. Se avete bisogno di storage efficiente, costruire i propri metodi di serializzazione personalizzato.

È stato utile?

Soluzione

Perché è forse interessare qualcuno, ho deciso di fare questo post su Che cosa significa il formato binario di serializzato .NET oggetti simile e come siamo in grado di interpretare correttamente?

Ho basato tutta la mia ricerca sul .NET Remoting: formato binario specifica struttura dati .


class Esempio:

Per avere un esempio di lavoro, ho creato una chiamata A semplice classe che contiene 2 proprietà, una stringa e un valore intero, essi sono chiamati SomeString e SomeValue.

Classe sguardi A come questo:

[Serializable()]
public class A
{
    public string SomeString
    {
        get;
        set;
    }

    public int SomeValue
    {
        get;
        set;
    }
}

Per la serializzazione ho usato il BinaryFormatter naturalmente:

BinaryFormatter bf = new BinaryFormatter();
StreamWriter sw = new StreamWriter("test.txt");
bf.Serialize(sw.BaseStream, new A() { SomeString = "abc", SomeValue = 123 });
sw.Close();

Come si può vedere, ho approvato una nuova istanza della classe A contenente abc e 123 come valori.


Esempio dati dei risultati:

Se guardiamo il risultato serializzato in un editor esadecimale, otteniamo qualcosa di simile:

i dati dei risultati Esempio


Cerchiamo di interpretare i dati Esempio Risultato:

In base alle specifiche di cui sopra (ecco il link diretto al PDF: [MS-NRBF] .pdf ) ogni record all'interno del flusso è identificato dal RecordTypeEnumeration. Sezione stati 2.1.2.1 RecordTypeNumeration:

  

Questa enumerazione identifica il tipo di record. Ogni record (tranne che per MemberPrimitiveUnTyped) inizia con un tipo di record di enumerazione. La dimensione del enumerazione è un byte.


SerializationHeaderRecord:

Quindi, se guardiamo indietro i dati che abbiamo ottenuto, possiamo iniziare a interpretare il primo byte:

SerializationHeaderRecord_RecordTypeEnumeration

Come affermato in 2.1.2.1 RecordTypeEnumeration un valore 0 identifica il SerializationHeaderRecord specificato in 2.6.1 SerializationHeaderRecord:

  

Il record SerializationHeaderRecord DEVE essere il primo record in una serializzazione binaria. Questo record è la versione maggiore e minore del formato e gli ID dell'oggetto superiore e le intestazioni.

E 'composto da:

  • RecordTypeEnum (1 byte)
  • RootId (4 byte)
  • HeaderId (4 byte)
  • MajorVersion (4 byte)
  • MinorVersion (4 byte)


Con questa conoscenza possiamo interpretare il record contenente 17 byte:

SerializationHeaderRecord_Complete

00 rappresenta il RecordTypeEnumeration che è SerializationHeaderRecord nel nostro caso.

01 00 00 00 rappresenta il RootId

  

Se né il record BinaryMethodCall né BinaryMethodReturn è presente nel flusso di serializzazione, il valore di questo campo deve contenere l'ObjectId di una Classe, Array, o registrare BinaryObjectString contenuti nel flusso di serializzazione.

Quindi, nel nostro caso questo dovrebbe essere il ObjectId con il 1 valore (perché i dati sono serializzato usando little-endian), che speriamo di vedere ancora una volta; -)

FF FF FF FF rappresenta il HeaderId

01 00 00 00 rappresenta il MajorVersion

00 00 00 00 rappresenta il MinorVersion
BinaryLibrary:

Come specificato, ogni record deve iniziare con la RecordTypeEnumeration. Come l'ultimo record è completa, si deve presumere che un nuovo inizio.
Cerchiamo di interpretare il byte successivo:

BinaryLibraryRecord_RecordTypeEnumeration

Come possiamo vedere, nel nostro esempio il SerializationHeaderRecord è seguito dal record BinaryLibrary:

  

I soci di record BinaryLibrary un Int32 ID (come specificato in [MS-DTYP] sezione 2.2.22) con un nome di libreria. Questo permette ad altri record riferimento il nome della libreria utilizzando l'ID. Questo approccio riduce la dimensione del filo quando ci sono più record che fanno riferimento lo stesso nome della libreria.

E 'composto da:

  • RecordTypeEnum (1 byte)
  • ID libreria (4 byte)
  • LibraryName (numero variabile di byte (che è un LengthPrefixedString))


Come indicato nel 2.1.1.6 LengthPrefixedString ...

  

Il LengthPrefixedString rappresenta un valore stringa. La stringa è preceduto dalla lunghezza della stringa codificata UTF-8 in byte. La lunghezza è codificato in un campo di lunghezza variabile, con un minimo di 1 byte e un massimo di 5 byte. Per ridurre al minimo le dimensioni dei cavi, la lunghezza è codificato come un campo di lunghezza variabile.

Nel nostro semplice esempio la lunghezza è sempre codificato utilizzando 1 byte. Con questa conoscenza possiamo continuare l'interpretazione dei byte nel flusso:

BinaryLibraryRecord_RecordTypeEnumeration_LibraryId

0C rappresenta la RecordTypeEnumeration che identifica il record BinaryLibrary.

02 00 00 00 rappresenta il LibraryId che è 2 nel nostro caso.


Ora il LengthPrefixedString segue:

BinaryLibraryRecord_RecordTypeEnumeration_LibraryId_LibraryName

42 rappresenta l'informazione sulla lunghezza del LengthPrefixedString che contiene il LibraryName.

Nel nostro caso le informazioni lunghezza 42 (decimale 66) dirci è che abbiamo bisogno di leggere i prossimi 66 byte e interpretarli come il LibraryName.

Come già detto, la stringa è UTF-8 codificata, quindi il risultato del byte sopra sarebbe qualcosa come: _WorkSpace_, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null


ClassWithMembersAndTypes:

Anche in questo caso, il record è completo in modo interpretiamo il RecordTypeEnumeration di quello successivo:

ClassWithMembersAndTypesRecord_RecordTypeEnumeration

05 identifica un record ClassWithMembersAndTypes. Sezione stati 2.3.2.1 ClassWithMembersAndTypes:

  

Il record ClassWithMembersAndTypes è la più verboso dei record di classe. Contiene i metadati relativi membri, compresi i nomi e Remoting Tipi dei membri. Esso contiene anche una biblioteca ID che fa riferimento il nome della libreria della Classe.

E 'composto da:

  • RecordTypeEnum (1 byte)
  • ClassInfo (numero variabile di byte)
  • MemberTypeInfo (numero variabile di byte)
  • ID libreria (4 byte)


ClassInfo:

Come indicato nel 2.3.1.1 ClassInfo il record è composto da:

  • ObjectId (4 byte)
  • Nome (numero variabile di byte (che è di nuovo un LengthPrefixedString))
  • MemberCount (4 byte)
  • MemberNames (che è una sequenza di LengthPrefixedString di cui il numero di elementi deve essere uguale al valore specificato nel campo MemberCount.)


Torna ai dati grezzi, passo dopo passo:

ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId

01 00 00 00 rappresenta il ObjectId. Abbiamo già visto questo, è stato specificato come RootId nella SerializationHeaderRecord.
ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId_Name

0F 53 74 61 63 6B 4F 76 65 72 46 6C 6F 77 2E 41 rappresenta la Name della classe rappresentata utilizzando un LengthPrefixedString. Come accennato, nel nostro esempio la lunghezza della stringa è definita con 1 byte così i primi byte specifica 0F che 15 byte devono essere letti e decodificati usando UTF-8. Gli sguardi di risultato qualcosa di simile: StackOverFlow.A - quindi ovviamente ho usato StackOverFlow il nome dello spazio dei nomi.
ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId_Name_MemberCount

02 00 00 00 rappresenta il MemberCount, è dirci è che 2 membri, entrambi rappresentati con LengthPrefixedString di seguiranno.
Nome del primo membro: ClassWithMembersAndTypesRecord_MemberNameOne

1B 3C 53 6F 6D 65 53 74 72 69 6E 67 3E 6B 5F 5F 42 61 63 6B 69 6E 67 46 69 65 6C 64 rappresenta il primo MemberName, 1B è di nuovo la lunghezza della stringa che è 27 byte un risultato simile a questo: <SomeString>k__BackingField.
Nome del secondo membro: ClassWithMembersAndTypesRecord_MemberNameTwo

1A 3C 53 6F 6D 65 56 61 6C 75 65 3E 6B 5F 5F 42 61 63 6B 69 6E 67 46 69 65 6C 64 rappresenta la seconda MemberName, specifica 1A che la stringa è lungo 26 byte. Essa si traduce in qualcosa di simile a questo:. <SomeValue>k__BackingField


MemberTypeInfo:

Dopo la ClassInfo il MemberTypeInfo segue.

Sezione 2.3.1.2 - MemberTypeInfo Stati, che la struttura contiene:

  • BinaryTypeEnums (di lunghezza variabile)
  

Una sequenza di valori BinaryTypeEnumeration che rappresenta i tipi dell'utente che vengono trasferiti. Il MUST Array:

     
      
  • hanno lo stesso numero di elementi come il campo MemberNames della struttura ClassInfo.

  •   
  • essere ordinato in modo tale che le corrisponde BinaryTypeEnumeration al nome del membro nel campo MemberNames della struttura ClassInfo.

  •   
  • AdditionalInfos (lunghezza variabile), a seconda del BinaryTpeEnum informazioni aggiuntive può o può non essere presente.
  

| BinaryTypeEnum | AdditionalInfos |
  |----------------+--------------------------|
  | Primitive | PrimitiveTypeEnumeration |
  | String | None |

Quindi, prendendo in considerazione che ci siamo quasi ... Ci aspettiamo 2 valori BinaryTypeEnumeration (perché avevamo 2 membri nel MemberNames).


Anche in questo caso, di nuovo ai dati grezzi del record MemberTypeInfo completo:

ClassWithMembersAndTypesRecord_MemberTypeInfo

01 rappresenta la BinaryTypeEnumeration del primo elemento, secondo 2.1.2.2 BinaryTypeEnumeration ci si può aspettare un String ed è rappresentato con un LengthPrefixedString.

00 rappresenta la BinaryTypeEnumeration del secondo elemento, e ancora, secondo la specifica, è un Primitive. Come detto sopra, di Primitive sono seguiti da ulteriori informazioni, in questo caso un PrimitiveTypeEnumeration. Ecco perché abbiamo bisogno di leggere il byte successivo, che è 08, abbinarlo con la tabella indicato nel 2.1.2.3 PrimitiveTypeEnumeration ed essere sorpreso a notare che ci si può aspettare un Int32 che è rappresentata da 4 byte, come indicato in un altro documento su tipi di dati di base.


ID libreria:

Dopo la MemerTypeInfo il LibraryId segue, è rappresentato da 4 byte:

ClassWithMembersAndTypesRecord_LibraryId

02 00 00 00 rappresenta il LibraryId che è 2.


I valori:

Come specificato nel 2.3 Class Records:

  

I valori dei membri della classe deve essere serializzata come record che seguono questo disco, come specificato nella sezione 2.7. L'ordine dei record devono corrispondere all'ordine di MemberNames come specificato nella struttura ClassInfo (sezione 2.3.1.1).

Ecco perché possiamo ora si aspettano i valori dei membri.
Vediamo gli ultimi pochi byte:

BinaryObjectStringRecord_RecordTypeEnumeration

06 identifica un BinaryObjectString. Esso rappresenta il valore della nostra proprietà SomeString (il <SomeString>k__BackingField per l'esattezza).

Secondo 2.5.7 BinaryObjectString contiene:

  • RecordTypeEnum (1 byte)
  • ObjectId (4 byte)
  • Valore (lunghezza variabile, rappresentato come un LengthPrefixedString)


Quindi, sapendo che, siamo in grado di identificare chiaramente che

BinaryObjectStringRecord_RecordTypeEnumeration_ObjectId_MemberOneValue

03 00 00 00 rappresenta il ObjectId.

03 61 62 63 rappresenta il Value dove 03 è la lunghezza della stringa stessa e 61 62 63 sono contenuti byte che si traducono in abc.

Speriamo che si può ricordare che c'era un secondo membro, un Int32. Sapendo che la Int32 è rappresentato utilizzando 4 byte, possiamo concludere che

BinaryObjectStringRecord_RecordTypeEnumeration_ObjectId_MemberOneValue_MemberTwoValue

deve essere il Value del nostro secondo membro. 7B esadecimale equivale decimale 123 che sembra essere adatto il nostro codice di esempio.

Così qui è il record ClassWithMembersAndTypes completo: ClassWithMembersAndTypesRecord_Complete


MessageEnd:

MessageEnd_RecordTypeEnumeration

Infine l'ultima 0B byte rappresenta il record MessageEnd.

Altri suggerimenti

Vasiliy è giusto nel senso che io alla fine bisogno di implementare il mio processo di formattazione / serializzazione a gestire meglio delle versioni e per emettere un flusso molto più compatto (prima della compressione).

volevo capire cosa stesse succedendo nel flusso, però, così ho scritto un (relativamente) rapida classe che fa quello che volevo:

  • analizza la sua strada attraverso il torrente, la costruzione di una collezione di nomi degli oggetti, conti e dimensioni
  • Una volta fatto, emette un breve riassunto di ciò che ha trovato - classi, i conteggi e le dimensioni totali nel flusso

Non è abbastanza utile per me di mettere da qualche parte visibile come CodeProject, quindi ho solo buttato il progetto in un file zip sul mio sito: http://www.architectshack.com/BinarySerializationAnalysis.ashx

Nel mio caso specifico si scopre che il problema era duplice:

  • Il BinaryFormatter è molto dettagliata (questo è noto, solo che non avevo capito l'entità)
  • I ha avuto problemi nella mia classe, si è scoperto che stavo memorizzare gli oggetti che non volevo

Spero che questo aiuti qualcuno ad un certo punto!


Aggiornamento: Ian Wright mi ha contattato con un problema con il codice originale, dove si schiantò quando l'oggetto di origine (s) conteneva valori "decimali". Questo è ora corretto, e ho colto l'occasione per spostare il codice per GitHub e dargli una licenza (permissiva, BSD).

La nostra applicazione funziona dati di massa. Si può richiedere fino a 1-2 GB di RAM, come il vostro gioco. Abbiamo incontrato lo stesso "memorizzazione di più copie dello stesso oggetto" problema. Anche i negozi di serializzazione binaria troppo metadati. Quando è stato implementato prima il file serializzato sono voluti circa 1-2 GB. Al giorno d'oggi sono riuscito a diminuire il valore - 50-100 MB. Che cosa abbiamo fatto.

La risposta breve - non utilizzare la serializzazione binaria .Net, creare il proprio meccanismo di serializzazione binaria Abbiamo propria classe BinaryFormatter e ISerializable (con due metodi Serializzazione, deserializzare)

Lo stesso oggetto non deve essere serializzato più di una volta. Risparmiamo E 'ID univoco e ripristinare l'oggetto dalla cache.

posso condividere un codice se chiedete.

Modifica Sembra lei ha ragione. Vedere il seguente codice - si dimostra che mi sbagliavo.

[Serializable]
public class Item
{
    public string Data { get; set; }
}

[Serializable]
public class ItemHolder
{
    public Item Item1 { get; set; }

    public Item Item2 { get; set; }
}

public class Program
{
    public static void Main(params string[] args)
    {
        {
            Item item0 = new Item() { Data = "0000000000" };
            ItemHolder holderOneInstance = new ItemHolder() { Item1 = item0, Item2 = item0 };

            var fs0 = File.Create("temp-file0.txt");
            var formatter0 = new BinaryFormatter();
            formatter0.Serialize(fs0, holderOneInstance);
            fs0.Close();
            Console.WriteLine("One instance: " + new FileInfo(fs0.Name).Length); // 335
            //File.Delete(fs0.Name);
        }

        {
            Item item1 = new Item() { Data = "1111111111" };
            Item item2 = new Item() { Data = "2222222222" };
            ItemHolder holderTwoInstances = new ItemHolder() { Item1 = item1, Item2 = item2 };

            var fs1 = File.Create("temp-file1.txt");
            var formatter1 = new BinaryFormatter();
            formatter1.Serialize(fs1, holderTwoInstances);
            fs1.Close();
            Console.WriteLine("Two instances: " + new FileInfo(fs1.Name).Length); // 360
            //File.Delete(fs1.Name);
        }
    }
}

appare come usi BinaryFormatter Object.equalsQ per trovare stessi oggetti.

Avete mai guardato dentro i file generati? Se si apre "temp-File0.txt" e "temp-file1.txt" dal codice di esempio vedrete che ha un sacco di metadati. È per questo che ti ho consigliato per creare il proprio meccanismo di serializzazione.

Ci scusiamo per il cofusing.

Forse si potrebbe eseguire il programma in modalità debug e provare ad aggiungere un punto di controllo.

Se questo è impossibile a causa della dimensione del gioco o di altre dipendenze si può sempre Coade una semplice piccola applicazione / che include il codice di deserializzazione e sbirciare dalla modalità di debug lì.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top