Como analisar o conteúdo do fluxo de serialização binária?
-
27-09-2019 - |
Pergunta
Estou usando a serialização binária (Bininaryformatter) como um mecanismo temporário para armazenar informações de estado em um arquivo para uma estrutura de objetos relativamente complexa (jogo); Os arquivos estão saindo Muito de Maior do que eu espero, e minha estrutura de dados inclui referências recursivas - por isso, estou me perguntando se o BinaryFormatter está realmente armazenando várias cópias dos mesmos objetos, ou se meu número básico de objetos e valores que eu deveria ter "aritmental é base, ou de onde mais o tamanho excessivo vem.
Pesquisando no Stack Overflow, consegui encontrar a especificação para o formato binário remoto binário da Microsoft:http://msdn.microsoft.com/en-us/library/cc236844(prot.10).aspx
O que não consigo encontrar é nenhum espectador existente que permita que você "espie" no conteúdo de um arquivo de saída Binaryformatter - obtenha contagens de objetos e bytes totais para diferentes tipos de objetos no arquivo, etc;
Eu sinto que esse deve ser o meu "Google -fu" falhando em mim (o pouco que tenho) - alguém pode ajudar? este devo já foram feitos antes, certo ??
ATUALIZAR: Não consegui encontrá -lo e não obtive respostas, então coloquei algo relativamente rápido (link para o projeto para download abaixo); Posso confirmar que o BinaryFormatter não armazena várias cópias do mesmo objeto, mas ele imprime muitos metadados no fluxo. Se você precisar de armazenamento eficiente, crie seus próprios métodos de serialização personalizados.
Solução
Porque talvez seja de interesse para alguém que eu decidi fazer este post sobre Como é o formato binário de objetos .NET serializados e como podemos interpretá -lo corretamente?
Eu baseei toda a minha pesquisa sobre o .NET Remoting: estrutura de dados de formato binário especificação.
Classe de exemplo:
Para ter um exemplo de funcionamento, criei uma classe simples chamada A
que contém 2 propriedades, uma corda e um valor inteiro, eles são chamados SomeString
e SomeValue
.
Classe A
se parece com isso:
[Serializable()]
public class A
{
public string SomeString
{
get;
set;
}
public int SomeValue
{
get;
set;
}
}
Para a serialização, usei o BinaryFormatter
é claro:
BinaryFormatter bf = new BinaryFormatter();
StreamWriter sw = new StreamWriter("test.txt");
bf.Serialize(sw.BaseStream, new A() { SomeString = "abc", SomeValue = 123 });
sw.Close();
Como pode ser visto, passei por uma nova instância de classe A
contendo abc
e 123
como valores.
Exemplo de dados do resultado:
Se olharmos para o resultado serializado em um editor hexadecimente, temos algo assim:
Vamos interpretar os dados de resultado de exemplo:
De acordo com a especificação acima mencionada (aqui está o link direto para o PDF: Ms-nrbf] .pdf) Cada registro dentro do fluxo é identificado pelo RecordTypeEnumeration
. Seção 2.1.2.1 RecordTypeNumeration
estados:
Essa enumeração identifica o tipo de registro. Cada registro (exceto para membros do PrimitiveUntyped) começa com uma enumeração de tipo de registro. O tamanho da enumeração é um byte.
SerializationHeaderCord:
Então, se olharmos para os dados que obtemos, podemos começar a interpretar o primeiro byte:
Como afirmado em 2.1.2.1 RecordTypeEnumeration
um valor de 0
identifica o SerializationHeaderRecord
que é especificado em 2.6.1 SerializationHeaderRecord
:
O registro da serializationHeaderCord deve ser o primeiro registro em uma serialização binária. Esse registro possui a versão principal e menor do formato e os IDs do objeto superior e dos cabeçalhos.
Isso consiste de:
- RecordTypeenum (1 byte)
- Rootid (4 bytes)
- Headerid (4 bytes)
- Majorversion (4 bytes)
- Minorversion (4 bytes)
Com esse conhecimento, podemos interpretar o registro contendo 17 bytes:
00
representa o RecordTypeEnumeration
qual é SerializationHeaderRecord
no nosso caso.
01 00 00 00
representa o RootId
Se nem o registro binarymethodCall nem binarymethodreturn estiver presente no fluxo de serialização, o valor desse campo deverá conter o objeto de uma classe, matriz ou registro binaryObjectString contido no fluxo de serialização.
Então, no nosso caso, esse deve ser o ObjectId
com o valor 1
(porque os dados são serializados usando pouco endiano), que esperamos ver novamente ;-)
FF FF FF FF
representa o HeaderId
01 00 00 00
representa o MajorVersion
00 00 00 00
representa o MinorVersion
Binária BILIBRARY:
Conforme especificado, cada registro deve começar com o RecordTypeEnumeration
. À medida que o último registro está concluído, devemos assumir que um novo começa.
Vamos interpretar o próximo byte:
Como podemos ver, em nosso exemplo o SerializationHeaderRecord
é seguido pelo BinaryLibrary
registro:
O registro binário associa um ID INT32 (conforme especificado na seção 2.2.22 [MS-DTYP]) ao nome da biblioteca. Isso permite que outros registros faça referência ao nome da biblioteca usando o ID. Essa abordagem reduz o tamanho do fio quando existem vários registros que fazem referência ao mesmo nome da biblioteca.
Isso consiste de:
- RecordTypeenum (1 byte)
- LibraryID (4 bytes)
- Biblioteca nome (número variável de bytes (que é um
LengthPrefixedString
))
Como afirmado em 2.1.1.6 LengthPrefixedString
...
O LengthPrefixedString representa um valor de string. A sequência é prefixada pelo comprimento da string codificada UTF-8 em bytes. O comprimento é codificado em um campo de comprimento variável com um mínimo de 1 byte e um máximo de 5 bytes. Para minimizar o tamanho do fio, o comprimento é codificado como um campo de comprimento variável.
Em nosso exemplo simples, o comprimento é sempre codificado usando 1 byte
. Com esse conhecimento, podemos continuar a interpretação dos bytes no fluxo:
0C
representa o RecordTypeEnumeration
que identifica o BinaryLibrary
registro.
02 00 00 00
representa o LibraryId
qual é 2
no nosso caso.
Agora o LengthPrefixedString
segue:
42
representa a informação de comprimento do LengthPrefixedString
que contém o LibraryName
.
No nosso caso, as informações de comprimento de 42
(decimal 66) nos diz que precisamos ler os próximos 66 bytes e interpretá -los como o LibraryName
.
Como já foi dito, a corda é UTF-8
codificado, então o resultado dos bytes acima seria algo como: _WorkSpace_, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
ClassWithMembersAndTypes:
Novamente, o registro está completo, então interpretamos o RecordTypeEnumeration
do próximo:
05
identifica a ClassWithMembersAndTypes
registro. Seção 2.3.2.1 ClassWithMembersAndTypes
estados:
O ClassWithMembersAndTypes Record é o mais detalhado dos registros de classe. Ele contém metadados sobre os membros, incluindo os nomes e tipos remotos dos membros. Ele também contém um ID da biblioteca que faz referência ao nome da biblioteca da classe.
Isso consiste de:
- RecordTypeenum (1 byte)
- Classinfo (número variável de bytes)
- MEMBORTYPEINFO (Número variável de bytes)
- LibraryID (4 bytes)
Classinfo:
Como afirmado em 2.3.1.1 ClassInfo
O registro consiste em:
- ObjectId (4 bytes)
- Nome (número variável de bytes (que é novamente um
LengthPrefixedString
)) - MemberCount (4 bytes)
- Nomes de membros (que é uma sequência de
LengthPrefixedString
onde o número de itens deve ser igual ao valor especificado noMemberCount
campo.)
De volta aos dados brutos, passo a passo:
01 00 00 00
representa o ObjectId
. Já vimos este, foi especificado como o RootId
no SerializationHeaderRecord
.
0F 53 74 61 63 6B 4F 76 65 72 46 6C 6F 77 2E 41
representa o Name
da classe que é representada usando um LengthPrefixedString
. Como mencionado, em nosso exemplo, o comprimento da corda é definido com 1 byte, então o primeiro byte 0F
Especifica que 15 bytes devem ser lidos e decodificados usando o UTF-8. O resultado se parece com o seguinte: StackOverFlow.A
- Então, obviamente, eu usei StackOverFlow
como nome do espaço para nome.
02 00 00 00
representa o MemberCount
, nos diz que 2 membros, ambos representados com LengthPrefixedString
's seguirá.
Nome do primeiro 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
representa o primeiro MemberName
, 1B
é novamente o comprimento da corda com 27 bytes de comprimento e resulta em algo assim: <SomeString>k__BackingField
.
Nome do segundo 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
representa o segundo MemberName
, 1A
Especifica que a string tem 26 bytes de comprimento. Resulta em algo assim: <SomeValue>k__BackingField
.
MembroTypeInfo:
Depois de ClassInfo
a MemberTypeInfo
segue.
Seção 2.3.1.2 - MemberTypeInfo
Estados, que a estrutura contém:
- BinaryTypeenums (variável em comprimento)
Uma sequência de valores de binarypeenumeration que representa os tipos de membros que estão sendo transferidos. A matriz deve:
Tenha o mesmo número de itens do campo Nomes de membros da estrutura Classinfo.
Seja ordenado de modo que a binarypeenumeration corresponda ao nome do membro no campo Nomes de membros da estrutura Classinfo.
- AdicionalInfos (variável em comprimento), dependendo do
BinaryTpeEnum
Informações adicionais podem ou não estar presentes.
| BinaryTypeEnum | AdditionalInfos |
|----------------+--------------------------|
| Primitive | PrimitiveTypeEnumeration |
| String | None |
Então, levando isso em consideração, estamos quase lá ... esperamos 2 BinaryTypeEnumeration
valores (porque tínhamos 2 membros no MemberNames
).
Novamente, de volta aos dados brutos do completo MemberTypeInfo
registro:
01
representa o BinaryTypeEnumeration
do primeiro membro, de acordo com 2.1.2.2 BinaryTypeEnumeration
Podemos esperar um String
e é representado usando um LengthPrefixedString
.
00
representa o BinaryTypeEnumeration
do segundo membro, e novamente, de acordo com a especificação, é um Primitive
. Como afirmado acima, Primitive
são seguidos por informações adicionais, neste caso a PrimitiveTypeEnumeration
. É por isso que precisamos ler o próximo byte, que é 08
, combine com a tabela declarada em 2.1.2.3 PrimitiveTypeEnumeration
e fique surpreso ao perceber que podemos esperar um Int32
que é representado por 4 bytes, conforme declarado em algum outro documento sobre tipos de dados básicos.
LibraryID:
Depois de MemerTypeInfo
a LibraryId
segue -se, é representado por 4 bytes:
02 00 00 00
representa o LibraryId
que é 2.
Os valores:
Conforme especificado em 2.3 Class Records
:
Os valores dos membros da classe devem ser serializados como registros que seguem esse registro, conforme especificado na Seção 2.7. A ordem dos registros deve corresponder à ordem dos nomes de membros, conforme especificado na estrutura Classinfo (Seção 2.3.1.1).
É por isso que agora podemos esperar os valores dos membros.
Vejamos os últimos bytes:
06
identifica um BinaryObjectString
. Representa o valor do nosso SomeString
propriedade (o <SomeString>k__BackingField
para ser exato).
De acordo com 2.5.7 BinaryObjectString
contém:
- RecordTypeenum (1 byte)
- ObjectId (4 bytes)
- Valor (comprimento variável, representado como um
LengthPrefixedString
)
Então, sabendo disso, podemos identificar claramente que
03 00 00 00
representa o ObjectId
.
03 61 62 63
representa o Value
Onde 03
é o comprimento da própria corda e 61 62 63
são os bytes de conteúdo que se traduzem em abc
.
Espero que você possa lembrar que havia um segundo membro, um Int32
. Sabendo disso Int32
é representado usando 4 bytes, podemos concluir que
deve ser o Value
do nosso segundo membro. 7B
Igualções hexadecimais 123
Decimal, que parece se encaixar no nosso código de exemplo.
Então aqui está o completo ClassWithMembersAndTypes
registro:
MessageEnd:
Finalmente o último byte 0B
representa o MessageEnd
registro.
Outras dicas
O Vasiliy está certo, pois, em última análise, precisarei implementar meu próprio processo de formatação/serialização para lidar melhor com a versão e para emitir um fluxo muito mais compacto (antes da compactação).
Eu queria entender o que estava acontecendo no fluxo, no entanto, então escrevi uma aula (relativamente) rápida que faz o que eu queria:
- Aparece o seu riacho, construindo coleções de nomes de objetos, conta e tamanhos
- Uma vez feito, produz um resumo rápido do que encontrou - classes, contagens e tamanhos totais no fluxo
Não é útil o suficiente para eu colocá -lo em algum lugar visível como o CodeProject, então eu apenas larguei o projeto em um arquivo zip no meu site: http://www.architectshack.com/binaryserializationAnálise.ashx
No meu caso específico, acontece que o problema era duplo:
- O Binário Formatter é muito detalhado (isso é conhecido, eu simplesmente não percebi a extensão)
- Eu tive problemas na minha aula, aconteceu que eu estava armazenando objetos que eu não queria
Espero que isso ajude alguém em algum momento!
Atualização: Ian Wright entrou em contato comigo com um problema com o código original, onde travou quando o (s) objeto (s) de origem (s) continha valores "decimais". Agora isso está corrigido, e eu usei a ocasião para mover o código para o GitHub e dar uma licença (permissiva, BSD).
Nosso aplicativo opera dados maciços. Pode levar até 1-2 GB de RAM, como o seu jogo. Encontramos o mesmo problema "armazenando várias cópias dos mesmos objetos". Também a serialização binária armazena muitos dados. Quando foi implementado pela primeira vez, o arquivo serializado levou cerca de 1-2 GB. Atualmente, consegui diminuir o valor - 50-100 MB. O que nós fizemos.
A resposta curta - não use a serialização binária .NET, crie seu próprio mecanismo de serialização binária. Temos uma classe BinaryFormatter e interface iserializável (com dois métodos serializam, desesteram).
O mesmo objeto não deve ser serializado mais de uma vez. Salvamos seu ID exclusivo e restauramos o objeto do cache.
Posso compartilhar algum código se você perguntar.
EDITAR: Parece que você está correto. Veja o código a seguir - prova que eu estava errado.
[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);
}
}
}
Parece BinaryFormatter
usa object.equals para encontrar os mesmos objetos.
Você já olhou para dentro dos arquivos gerados? Se você abrir "Temp-File0.txt" e "Temp-File1.txt" do exemplo do código, você verá que possui muitos meta-dados. É por isso que eu recomendei que você criasse seu próprio mecanismo de serialização.
Desculpe por estar cofusando.
Talvez você possa executar seu programa no modo de depuração e tentar adicionar um ponto de controle.
Se isso for impossível devido ao tamanho do jogo ou a outras dependências, você sempre pode coar um aplicativo simples/pequeno que inclui o código de deseralização e espiar do modo de depuração lá.