Come analizzare i contenuti del flusso di serializzazione binario?
-
27-09-2019 - |
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.
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:
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:
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:
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:
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:
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:
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:
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 campoMemberCount
.)
Torna ai dati grezzi, passo dopo passo:
01 00 00 00
rappresenta il ObjectId
. Abbiamo già visto questo, è stato specificato come RootId
nella SerializationHeaderRecord
.
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.
02 00 00 00
rappresenta il MemberCount
, è dirci è che 2 membri, entrambi rappresentati con LengthPrefixedString
di seguiranno.
Nome del primo membro:
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:
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:
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:
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:
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
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
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:
MessageEnd:
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ì.