serializar serializacion objeto ejemplo deserializacion binaria c# .net serialization binary-serialization

objeto - serializacion y deserializacion c#



¿Cómo analizar los contenidos de la secuencia de serialización binaria? (4)

Estoy utilizando la serialización binaria (BinaryFormatter) como un mecanismo temporal para almacenar información de estado en un archivo para una estructura de objetos (juegos) relativamente compleja; los archivos están saliendo mucho más grandes de lo que esperaba, y mi estructura de datos incluye referencias recursivas, por lo que me pregunto si BinaryFormatter realmente está almacenando varias copias de los mismos objetos, o si mi "número de objetos y valores básicos" debería tener "La aritmética está fuera de la base, o de dónde más viene el tamaño excesivo.

Buscando en el desbordamiento de la pila, pude encontrar la especificación para el formato de comunicación remota binaria de Microsoft: http://msdn.microsoft.com/en-us/library/cc236844(PROT.10).aspx

Lo que no puedo encontrar es un visor existente que te permita "echar un vistazo" al contenido de un archivo binario de salida de formato: obtener recuentos de objetos y bytes totales para diferentes tipos de objetos en el archivo, etc .;

Siento que este debe ser mi "google-fu" que me falla (lo poco que tengo) - ¿Alguien puede ayudar? Esto debe haberse hecho antes, ¿verdad?

ACTUALIZACIÓN : No pude encontrarlo y no obtuve ninguna respuesta, así que puse algo relativamente rápido junto (enlace al proyecto descargable a continuación); Puedo confirmar que BinaryFormatter no almacena varias copias del mismo objeto, pero imprime bastantes metadatos a la transmisión. Si necesita un almacenamiento eficiente, cree sus propios métodos de serialización personalizados.


Debido a que tal vez sea de interés para alguien, decidí hacer esta publicación sobre ¿Qué aspecto tiene el formato binario de los objetos .NET serializados y cómo podemos interpretarlo correctamente?

He basado toda mi investigación en .NET Remoting: especificación de estructura de datos de formato binario .



Clase de ejemplo:

Para tener un ejemplo de trabajo, he creado una clase simple llamada A que contiene 2 propiedades, una cadena y un valor entero, se llaman SomeString y SomeValue .

La clase A ve así:

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

Para la serialización utilicé BinaryFormatter por supuesto:

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

Como se puede ver, pasé una nueva instancia de clase A contiene abc y 123 como valores.



Ejemplo de datos de resultado:

Si miramos el resultado serializado en un editor hexadecimal, obtenemos algo como esto:



Permítanos interpretar los datos del resultado del ejemplo:

De acuerdo con la especificación mencionada anteriormente (aquí está el enlace directo al PDF: [MS-NRBF].pdf ) cada registro dentro de la secuencia se identifica mediante RecordTypeEnumeration . Sección 2.1.2.1 RecordTypeNumeration Estados de 2.1.2.1 RecordTypeNumeration :

Esta enumeración identifica el tipo de registro. Cada registro (excepto MemberPrimitiveUnTyped) comienza con una enumeración de tipo de registro. El tamaño de la enumeración es un BYTE.



SerializationHeaderRecord:

Entonces, si miramos hacia atrás en los datos que tenemos, podemos comenzar a interpretar el primer byte:

Como se indica en 2.1.2.1 RecordTypeEnumeration un valor de 0 identifica SerializationHeaderRecord que se especifica en 2.6.1 SerializationHeaderRecord :

El registro SerializationHeaderRecord DEBE ser el primer registro en una serialización binaria. Este registro tiene la versión principal y secundaria del formato y los ID del objeto superior y los encabezados.

Consiste en:

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



Con ese conocimiento podemos interpretar el registro que contiene 17 bytes:

00 representa la RecordTypeEnumeration que es SerializationHeaderRecord en nuestro caso.

01 00 00 00 representa el RootId

Si ni el registro BinaryMethodCall ni el registro BinaryMethodReturn están presentes en la secuencia de serialización, el valor de este campo DEBE contener la Id. Objeto de una clase, matriz o secuencia BinaryObjectString contenida en la secuencia de serialización.

Entonces, en nuestro caso, este debería ser el ObjectId con el valor 1 (porque los datos se serializan usando little-endian) que esperamos ver nuevamente ;-)

FF FF FF FF representa el HeaderId

01 00 00 00 representa la MajorVersion

00 00 00 00 representa la MinorVersion



BinaryLibrary:

Como se especifica, cada registro debe comenzar con RecordTypeEnumeration . A medida que se completa el último registro, debemos asumir que comienza uno nuevo.

Permítanos interpretar el siguiente byte:

Como podemos ver, en nuestro ejemplo, SerializationHeaderRecord es seguido por el registro BinaryLibrary :

El registro BinaryLibrary asocia una ID INT32 (como se especifica en [MS-DTYP] sección 2.2.22) con un nombre de biblioteca. Esto permite que otros registros hagan referencia al nombre de la Biblioteca utilizando la ID. Este enfoque reduce el tamaño del cable cuando hay varios registros que hacen referencia al mismo nombre de la Biblioteca.

Consiste en:

  • RecordTypeEnum (1 byte)
  • LibraryId (4 bytes)
  • LibraryName (número de variable de bytes (que es un LengthPrefixedString ))



Como se indica en 2.1.1.6 LengthPrefixedString ...

LengthPrefixedString representa un valor de cadena. La cadena está prefijada por la longitud de la cadena codificada en UTF-8 en bytes. La longitud está codificada en un campo de longitud variable con un mínimo de 1 byte y un máximo de 5 bytes. Para minimizar el tamaño del cable, la longitud se codifica como un campo de longitud variable.

En nuestro ejemplo simple, la longitud siempre está codificada usando 1 byte . Con ese conocimiento podemos continuar la interpretación de los bytes en la secuencia:

0C representa la RecordTypeEnumeration que identifica el registro BinaryLibrary .

02 00 00 00 representa la LibraryId que es 2 en nuestro caso.



Ahora el LengthPrefixedString sigue:

42 representa la información de longitud de LengthPrefixedString que contiene el LibraryName .

En nuestro caso, la información de longitud de 42 (decimal 66) nos dice que necesitamos leer los siguientes 66 bytes e interpretarlos como LibraryName .

Como ya se dijo, la cadena está UTF-8 , por lo que el resultado de los bytes anteriores sería algo así como: _WorkSpace_, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null



ClassWithMembersAndTypes:

Una vez más, el registro está completo, por lo que interpretamos el RecordTypeEnumeration del siguiente:

05 identifica un registro de ClassWithMembersAndTypes . La sección 2.3.2.1 ClassWithMembersAndTypes estados de 2.3.2.1 ClassWithMembersAndTypes :

El registro ClassWithMembersAndTypes es el más detallado de los registros de la clase. Contiene metadatos sobre los Miembros, incluidos los nombres y los tipos de Remoting de los miembros. También contiene una ID de biblioteca que hace referencia al nombre de la biblioteca de la clase.

Consiste en:

  • RecordTypeEnum (1 byte)
  • ClassInfo (número variable de bytes)
  • MemberTypeInfo (número variable de bytes)
  • LibraryId (4 bytes)



ClassInfo:

Como se indica en 2.3.1.1 ClassInfo el registro consta de:

  • ObjectId (4 bytes)
  • Nombre (número variable de bytes (que nuevamente es un LengthPrefixedString ))
  • MemberCount (4 bytes)
  • MemberNames (que es una secuencia de LengthPrefixedString donde el número de elementos DEBE ser igual al valor especificado en el campo MemberCount ).



Volver a los datos sin procesar, paso a paso:

01 00 00 00 representa el ObjectId . Ya hemos visto este, se especificó como RootId en SerializationHeaderRecord .

0F 53 74 61 63 6B 4F 76 65 72 46 6C 6F 77 2E 41 representa el Name de la clase que se representa mediante el uso de una LengthPrefixedString . Como se mencionó, en nuestro ejemplo, la longitud de la cadena se define con 1 byte, por lo que el primer byte 0F especifica que se deben leer y decodificar 15 bytes con UTF-8. El resultado se ve así: .A , así que obviamente utilicé como nombre del espacio de nombres.

02 00 00 00 representa MemberCount , nos dice que 2 miembros, ambos representados con LengthPrefixedString seguirán.

Nombre del primer miembro:

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 el primer MemberName , 1B es de nuevo la longitud de la cadena que tiene 27 bytes de longitud y da como resultado algo como esto: <SomeString>k__BackingField .

Nombre del segundo miembro:

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 el segundo MemberName , 1A especifica que la cadena tiene 26 bytes de longitud. Resulta en algo como esto: <SomeValue>k__BackingField .



MemberTypeInfo:

Después de MemberTypeInfo sigue MemberTypeInfo .

Sección 2.3.1.2 - MemberTypeInfo declara que la estructura contiene:

  • BinaryTypeEnums (variable en longitud)

Una secuencia de valores BinaryTypeEnumeration que representa los tipos de miembros que se transfieren. La matriz DEBE:

  • Tener el mismo número de elementos que el campo MemberNames de la estructura ClassInfo.

  • Ordénelo de modo que BinaryTypeEnumeration corresponda al nombre del miembro en el campo MemberNames de la estructura ClassInfo.

  • AdditionalInfos (variable en longitud), dependiendo de la BinaryTpeEnum adicional de BinaryTpeEnum puede o no estar presente.

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

Así que, teniendo eso en cuenta, ya casi llegamos ... Esperamos 2 valores de BinaryTypeEnumeration (porque teníamos 2 miembros en los MemberNames ).



Nuevamente, volviendo a los datos brutos del registro completo MemberTypeInfo :

01 representa la BinaryTypeEnumeration del primer miembro, de acuerdo con 2.1.2.2 BinaryTypeEnumeration podemos esperar una String y se representa utilizando un LengthPrefixedString .

00 representa la BinaryTypeEnumeration del segundo miembro, y de nuevo, de acuerdo con la especificación, es un Primitive . Como se indicó anteriormente, los Primitive son seguidos por información adicional, en este caso, una PrimitiveTypeEnumeration . Es por eso que tenemos que leer el siguiente byte, que es 08 , relacionarlo con la tabla indicada en 2.1.2.3 PrimitiveTypeEnumeration y sorprendernos al notar que podemos esperar un Int32 que esté representado por 4 bytes, como se indica en algún otro documento sobre tipos de datos básicos.



LibraryId:

Después de MemerTypeInfo sigue LibraryId , está representado por 4 bytes:

02 00 00 00 representa el LibraryId que es 2.



Los valores:

Como se especifica en 2.3 Class Records :

Los valores de los miembros de la clase DEBEN serializarse como registros que siguen este registro, como se especifica en la sección 2.7. El orden de los registros DEBE coincidir con el orden de MemberNames como se especifica en la estructura ClassInfo (sección 2.3.1.1).

Es por eso que ahora podemos esperar los valores de los miembros.

Echemos un vistazo a los últimos bytes:

06 identifica un BinaryObjectString . Representa el valor de nuestra propiedad SomeString (el <SomeString>k__BackingField para ser exactos).

De acuerdo con 2.5.7 BinaryObjectString contiene:

  • RecordTypeEnum (1 byte)
  • ObjectId (4 bytes)
  • Valor (longitud variable, representado como LengthPrefixedString )



Entonces, sabiendo eso, podemos identificar claramente eso

03 00 00 00 representa el ObjectId .

03 61 62 63 representa el Value donde 03 es la longitud de la cadena misma y 61 62 63 son los bytes de contenido que se traducen en abc .

Espero que puedas recordar que hubo un segundo miembro, un Int32 . Sabiendo que el Int32 está representado por el uso de 4 bytes, podemos concluir, que

debe ser el Value de nuestro segundo miembro. 7B hexadecimal es igual a 123 decimal que parece ajustarse a nuestro código de ejemplo.

Así que aquí está el registro completo de ClassWithMembersAndTypes :



MessageEnd:

Finalmente, el último byte 0B representa el registro de MessageEnd .


Nuestra aplicación opera datos masivos. Puede tomar hasta 1-2 GB de RAM, como su juego. Nos encontramos con el mismo problema de "almacenamiento de copias múltiples de los mismos objetos". Además, la serialización binaria almacena demasiados metadatos. Cuando se implementó por primera vez, el archivo serializado tomó aproximadamente 1-2 GB. Hoy en día logré disminuir el valor - 50-100 MB. Qué hicimos.

La respuesta corta: no use la serialización binaria .Net, cree su propio mecanismo de serialización binario. Tenemos nuestra propia clase BinaryFormatter y la interfaz ISerializable (con dos métodos Serialize, Deserialize).

El mismo objeto no se debe serializar más de una vez. Guardamos su identificación única y restauramos el objeto de la memoria caché.

Puedo compartir algunos códigos si me preguntas.

EDITAR: Parece que tienes razón. Vea el siguiente código: demuestra que estaba equivocado.

[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 que BinaryFormatter usa object.Equals para encontrar los mismos objetos.

¿Alguna vez ha buscado dentro de los archivos generados? Si abre "temp-file0.txt" y "temp-file1.txt" en el ejemplo del código, verá que tiene muchos metadatos. Es por eso que te recomendé que crees tu propio mecanismo de serialización.

Perdón por estar cofusionando.


Tal vez podría ejecutar su programa en modo de depuración e intentar agregar un punto de control.

Si eso es imposible debido al tamaño del juego u otras dependencias, siempre puedes usar una aplicación simple / pequeña que incluya el código de deserialización y echar un vistazo desde el modo de depuración.


Vasiliy tiene razón en que, en última instancia, necesitaré implementar mi propio proceso de formateo / serialización para manejar mejor el control de versiones y para producir una transmisión mucho más compacta (antes de la compresión).

Sin embargo, quería entender qué estaba pasando en la transmisión, así que redacté una clase (relativamente) rápida que hace lo que quería:

  • analiza su camino a través de la corriente, construyendo una colección de nombres de objetos, recuentos y tamaños
  • Una vez hecho, muestra un resumen rápido de lo que encontró: clases, recuentos y tamaños totales en la transmisión.

No es lo suficientemente útil para ponerlo en un lugar visible como el proyecto de código, así que acabo de abandonar el proyecto en un archivo zip en mi sitio web: http://www.architectshack.com/BinarySerializationAnalysis.ashx

En mi caso específico, resulta que el problema era doble:

  • BinaryFormatter es MUY prolijo (esto es conocido, simplemente no me di cuenta de la extensión)
  • Tuve problemas en mi clase, resultó que estaba almacenando objetos que no quería

Espero que esto ayude a alguien en algún momento!

Actualización: Ian Wright contactó conmigo con un problema con el código original, donde se colgó cuando el objeto fuente contenía valores "decimales". Esto ahora está corregido, y aproveché la ocasión para mover el código a GitHub y darle una licencia (permisiva, BSD).