Entendiendo la serialización


38

Soy ingeniero de software y después de una discusión con algunos colegas, me di cuenta de que no tengo una buena comprensión del concepto de serialización. Según tengo entendido, la serialización es el proceso de convertir alguna entidad, como un objeto en OOP, en una secuencia de bytes, de modo que dicha entidad pueda almacenarse o transmitirse para su posterior acceso (el proceso de "deserialización").

El problema que tengo es: ¿no están todas las variables (ya sean primitivas intu objetos compuestos) ya representadas por una secuencia de bytes? (Por supuesto que sí, porque están almacenados en registros, memoria, disco, etc.)

Entonces, ¿qué hace que la serialización sea un tema tan profundo? Para serializar una variable, ¿no podemos simplemente tomar estos bytes en la memoria y escribirlos en un archivo? ¿Qué complejidades me he perdido?


21
La serialización puede ser trivial para objetos contiguos . Cuando el valor del objeto se representa como un gráfico de puntero , las cosas se vuelven mucho más difíciles, especialmente si dicho gráfico tiene bucles.
chi

1
@chi: Tu primera oración es un poco engañosa dado que la contigüidad es irrelevante. Es posible que tenga un gráfico que sea continuo en la memoria y que aún no lo ayude a serializarlo, ya que aún tiene que (a) detectar que realmente es contiguo y (b) arreglar los punteros en el interior. Solo diría la segunda parte de lo que dijiste.
Mehrdad

@Mehrdad Estoy de acuerdo en que mi comentario no es completamente preciso, por las razones que mencionas. Tal vez sin puntero / usar puntero es una mejor distinción (incluso si no es completamente precisa, tampoco)
chi

77
También debe preocuparse por la representación en hardware. Si serializo un int 4 bytesen mi PDP-11 y luego intento leer esos mismos cuatro bytes en la memoria de mi macbook, no son el mismo número (debido a Endianes). Por lo tanto, debe normalizar los datos a una representación que pueda descodificar (esto es serialización). La forma en que serializa los datos también tiene compensaciones de velocidad / flexibilidad humana / máquina legible.
Martin York

¿Qué sucede si está utilizando Entity Framework con muchas propiedades de navegación profundamente conectadas? En un caso, es posible que desee serializar una propiedad de navegación, pero en otro dejarla nula (porque volverá a cargar ese objeto real de la base de datos en función de la ID que está en su objeto primario serializado). Esto es sólo un ejemplo. Hay muchos.
ErikE

Respuestas:


40

Si tiene una estructura de datos complicada, su representación en la memoria normalmente podría estar dispersa por toda la memoria. (Piense en un árbol binario, por ejemplo).

Por el contrario, cuando desea escribirlo en el disco, probablemente desee tener una representación como una secuencia (con suerte corta) de bytes contiguos. Eso es lo que la serialización hace por ti.


27

El problema que tengo es: ¿no están todas las variables (ya sean primitivas como int u objetos compuestos) ya representadas por una secuencia de bytes? (Por supuesto que sí, porque están almacenados en registros, memoria, disco, etc.)

Entonces, ¿qué hace que la serialización sea un tema tan profundo? Para serializar una variable, ¿no podemos simplemente tomar estos bytes en la memoria y escribirlos en un archivo? ¿Qué complejidades me he perdido?

Considere un gráfico de objeto en C con nodos definidos como este:

struct Node {
    struct Node* parent;
    struct Node* someChild;
    struct Node* anotherLink;

    int value;
    char* label;
};

//

struct Node nodes[10] = {0};
nodes[5].parent = nodes[0];
nodes[0].someChild = calloc( 1, sizeof(struct Node) );
nodes[5].anotherLink = nodes[3];
for( size_t i = 3; i < 7; i++ ) {
    nodes[i].anotherLink = calloc( 1, sizeof(struct Node) );
}

En tiempo de ejecución, todo el objeto Node gráfico del se dispersaría por el espacio de memoria, y el mismo nodo podría apuntar desde muchos nodos diferentes.

No puede simplemente volcar la memoria en un archivo / secuencia / disco y llamarlo serializado porque los valores del puntero (que son direcciones de memoria) no se pueden deserializar (porque esas ubicaciones de memoria podrían estar ocupadas cuando carga el volcado de nuevo) en la memoria). Otro problema con simplemente volcar la memoria es que terminarás almacenando todo tipo de datos irrelevantes y espacio no utilizado: en x86 un proceso tiene hasta 4GiB de espacio de memoria, y un sistema operativo o MMU solo tiene una idea general de qué memoria es realmente significativo o no (basado en las páginas de memoria asignadas a un proceso), por lo que tenerNotepad.exe volcar 4 GB de bytes sin procesar en mi disco cada vez que quiero guardar un archivo de texto parece un poco inútil.

Otro problema es con el control de versiones: ¿qué sucede si serializa su Nodegráfico el día 1 y luego el día 2 agrega otro campo aNode (como otro valor de puntero o un valor primitivo), luego el día 3 des-serializa su archivo de ¿día 1?

También debes considerar otras cosas, como el endianness. Una de las razones principales por las que los archivos MacOS e IBM / Windows / PC eran incompatibles entre sí en las décadas de 1980 y 1990 a pesar de que aparentemente los hicieron los mismos programas (Word, Photoshop, etc.) fue porque en valores enteros de múltiples bytes x86 / PC se guardaron en orden little-endian, pero en orden big-endian en Mac, y el software no se creó teniendo en cuenta la portabilidad multiplataforma. Hoy en día las cosas mejoran gracias a una mejor educación para desarrolladores y a nuestro mundo informático cada vez más heterogéneo.


2
Volcar todo en el espacio de memoria del proceso también sería horrible por razones de seguridad. Un programa nocturno tiene en memoria tanto 1) algunos datos públicos como 2) contraseña, nonce secreto o clave privada. Al serializar el primero, uno no quiere revelar ninguna información sobre el segundo.
chi


15

El truco ya está descrito en la propia palabra: " serial ización".

La pregunta es básicamente: ¿cómo puedo representar una gráfica dirigida cíclica interconectada arbitrariamente compleja de objetos complejos arbitrariamente como una secuencia lineal de bytes?

Piénselo: una secuencia lineal es algo así como un gráfico dirigido degenerado donde cada vértice tiene exactamente un borde entrante y saliente (excepto el "primer vértice" que no tiene borde entrante y el "último vértice" que no tiene borde saliente) . Y un byte es obviamente menos complejo que un objeto .

Por lo tanto, parece razonable que a medida que se pasa de un gráfico arbitrariamente complejo a una mucho más restringida "graph" (en realidad sólo una lista) y de los objetos arbitrariamente complejas a simples bytes, la información de voluntad perderá, si hacemos esto ingenuamente y no lo hacemos ' t codifica la información "extraña" de alguna manera. Y eso es exactamente lo que hace la serialización: codificar la información compleja en un formato lineal simple.

Si está familiarizado con YAML , puede echar un vistazo a las características de ancla y alias que le permiten representar la idea de que "el mismo objeto puede aparecer en diferentes lugares" en una serialización.

Por ejemplo, si tiene el siguiente gráfico:

A → B → D
↓       ↑
C ––––––+

Podría representar eso como una lista de rutas lineales en YAML como esta:

- [&A A, B, &D D]
- [*A, C, *D]

También puede representarlo como una lista de adyacencia, o una matriz de adyacencia, o como un par cuyo primer elemento es un conjunto de nodos y cuyo segundo elemento es un conjunto de pares de nodos, pero en todas esas representaciones, debe tener una forma de referirse hacia atrás y hacia adelante a los nodos existentes , es decir, punteros , que generalmente no tiene en un archivo o una secuencia de red. Todo lo que tienes, al final, son bytes.

(Lo que por cierto significa que el archivo de texto YAML anterior también debe ser "serializado", para eso están las diversas codificaciones de caracteres y formatos de transferencia Unicode ... no es estrictamente "serialización", solo codificación, porque el archivo de texto ya es una serie / lista lineal de puntos de código, pero puede ver algunas similitudes).


13

Las otras respuestas ya abordan gráficos de objetos complejos, pero vale la pena señalar que la serialización de primitivas tampoco es trivial.

Usando nombres de tipo primitivo C para concreción, considere:

  1. Yo serializo a long. Algún tiempo después, lo deserialicé, pero ... en una plataforma diferente, y ahora longestá en int64_tlugar del int32_tque almacené. Por lo tanto, debo tener mucho cuidado con el tamaño exacto de cada tipo que almaceno o almacenar algunos metadatos que describan el tipo y el tamaño de cada campo.

    Tenga en cuenta que esta plataforma diferente podría ser la misma plataforma después de una compilación futura.

  2. Yo serializo un int32_t. Algún tiempo después, lo des-serializo, pero ... en una plataforma diferente, y ahora el valor está corrupto. Lamentablemente guardé el valor en una plataforma big-endian, y lo cargué en una plataforma little-endian. Ahora necesito establecer una convención para mi formato o agregar más metadatos que describan la duración de cada archivo / secuencia / lo que sea. Y, por supuesto, realmente realiza las conversiones apropiadas.

  3. Yo serializo una cadena. Esta vez, una plataforma usa charUTF-8 y una wchar_ty UTF-16.

Por lo tanto, afirmaría que la serialización de calidad razonable no es trivial incluso para las primitivas en la memoria contigua. Hay muchas decisiones de codificación que necesita documentar o describir con metadatos en línea.

Los gráficos de objetos agregan otra capa de complejidad además de eso.


6

Hay múltiples aspectos:

Legibilidad por el mismo programa

Su programa ha almacenado sus datos de alguna manera como bytes en la memoria. Pero podría estar disperso arbitrariamente en diferentes registros, con punteros yendo y viniendo entre sus piezas más pequeñas [editar: Como se comentó, físicamente los datos son más probables en la memoria principal que un registro de datos, pero eso no elimina el problema del puntero] . Solo piense en una lista entera vinculada. Cada elemento de la lista puede almacenarse en un lugar totalmente diferente y todo lo que mantiene la lista unida son los punteros de un elemento al siguiente. Si tomara esos datos tal como están e intente copiarlos en otra máquina que ejecute el mismo programa, tendría problemas:

  1. En primer lugar, el registro indica que sus datos almacenados en una máquina ya podrían usarse para algo completamente diferente en otra máquina (alguien está explorando el intercambio de pila y el navegador ya se ha comido toda esa memoria). Entonces, si simplemente anula esos registros, adiós navegador. Por lo tanto, necesitaría reorganizar los punteros en la estructura para que se ajusten a las direcciones que tiene libres en la segunda máquina. El mismo problema surge cuando intenta volver a cargar los datos en la misma máquina más adelante.
  2. ¿Qué sucede si algunos componentes externos apuntan a su estructura o su estructura tiene punteros a datos externos, que no transmitió? ¡Segfaults por todas partes! Esto se convertiría en una pesadilla de depuración.

Legibilidad por otro programa

Supongamos que logra asignar las direcciones correctas en otra máquina, para que sus datos encajen. Si sus datos son procesados ​​por un programa separado en esa máquina (idioma diferente), ese programa podría tener una comprensión básica de los datos totalmente diferente. Supongamos que tiene objetos C ++ con punteros, pero su idioma de destino ni siquiera admite punteros en ese nivel. Una vez más, terminas sin una forma limpia de abordar esos datos en el segundo programa. Termina con algunos datos binarios en la memoria, pero luego, necesita escribir código adicional que envuelva los datos y de alguna manera los traduzca en algo con lo que su idioma de destino pueda trabajar. Suena como deserialización, solo que su punto de partida ahora es un objeto extraño disperso por su memoria principal, que es diferente para diferentes idiomas de origen, en lugar de un archivo con una estructura bien definida. Lo mismo, por supuesto, si intenta interpretar directamente el archivo binario que incluye punteros: debe escribir analizadores para cada forma posible en que otro idioma pueda representar datos en la memoria.

Legibilidad por un humano

Dos de los lenguajes de serialización modernos más destacados para la serialización basada en web (xml, json) son fácilmente entendibles por un humano. En lugar de una pila binaria de sustancia pegajosa, la estructura y el contenido reales de los datos son claros, incluso sin un programa para leer los datos. Esto tiene múltiples ventajas:

  • depuración más fácil -> si hay un problema en su cartera de servicios, simplemente mire los datos que salen de un servicio y verifique si tiene sentido (como primer paso); también puedes ver directamente si los datos se ven como crees que deberían, cuando escribes tu interfaz de exportación en primer lugar.
  • archivabilidad: si tiene sus datos como una pila pura de binarios, y pierde el programa destinado a interpretarlos, pierde los datos (o tendrá que pasar bastante tiempo para encontrar algo allí); Si sus datos serializados son legibles para humanos, puede usarlos fácilmente como un archivo o programar su propio importador para un nuevo programa
  • la naturaleza declarativa de los datos serializados de tal manera, también significa que es totalmente independiente del sistema informático y su hardware; podrías cargarlo en una computadora cuántica totalmente diferente o infectar una IA alienígena con hechos alternativos para que accidentalmente vuele al próximo sol (Emmerich si lees esto, una referencia sería buena, si usas esa idea para el próximo 4 de julio película)

Mis datos probablemente estén principalmente en la memoria principal, no en registros. Si mis datos encajan en los registros, la serialización apenas es un problema. Creo que has entendido mal lo que es un registro.
David Richerby

De hecho, utilicé el término registro demasiado libremente aquí. Pero el punto principal es que sus datos pueden contener punteros al espacio de direcciones para identificar sus propios componentes o para hacer referencia a otros datos. No importa si es un registro físico o una dirección virtual en la memoria principal.
Frank Hopkins

No, utilizó el término "registrarse" de forma completamente incorrecta. Las cosas que está llamando registros están en una parte completamente diferente de la jerarquía de la memoria a los registros reales.
David Richerby

6

Además de lo que han dicho las otras respuestas:

A veces quieres serializar cosas que no son datos puros.

Por ejemplo, piense en un identificador de archivo o una conexión a un servidor. Aunque el identificador de archivo o el socket es un int, este número no tiene sentido la próxima vez que se ejecute el programa. Para recrear correctamente los objetos que contienen identificadores para tales cosas, debe volver a abrir archivos y volver a crear conexiones, y decidir qué hacer si esto falla.

Actualmente, muchos idiomas admiten el almacenamiento de funciones anónimas dentro de objetos, por ejemplo, un onBlah()controlador en Javascript. Esto es desafiante porque dicho código puede contener referencias a datos adicionales que a su vez necesitan ser serializados. (Y luego está el problema de serializar código de una manera multiplataforma, que obviamente es más fácil para los idiomas interpretados). Aún así, incluso si solo se puede admitir un subconjunto del idioma, aún puede resultar bastante útil. No muchos mecanismos de serialización intentan serializar el código, pero consulte serialize-javascript .

En los casos en que desea serializar un objeto pero contiene algo que no es compatible con su mecanismo de serialización, debe volver a escribir el código de una manera que funcione alrededor de esto. Por ejemplo, puede usar enumeraciones en lugar de funciones anónimas cuando hay un número finito de funciones posibles.

A menudo, desea que los datos serializados sean concisos.

Si envía datos a través de la red o incluso los almacena en el disco, puede ser importante mantener el tamaño pequeño. Una de las formas más fáciles de lograr esto es desechar la información que se puede reconstruir (por ejemplo, descartar cachés, tablas hash y representaciones alternativas de los mismos datos).

Por supuesto, el programador debe seleccionar manualmente lo que se va a guardar y lo que se debe descartar, y asegurarse de que las cosas se reconstruyan cuando se recrea el objeto.

Piensa en el acto de guardar un juego. Los objetos pueden contener muchos punteros a datos gráficos, datos de sonido y otros objetos. Pero la mayoría de estas cosas se pueden cargar desde los archivos de datos del juego y no es necesario almacenarlas en un archivo guardado. Descartarlo puede ser laborioso, por lo que a menudo se dejan pequeñas cosas. He editado hexadecimalmente algunos archivos guardados en mi tiempo y descubrí datos que eran claramente redundantes, como descripciones textuales de elementos.

A veces el espacio no es importante, pero la legibilidad sí lo es, en cuyo caso puede usar un formato ASCII (posiblemente JSON o XML).


3

Definamos qué es realmente una secuencia de bytes. Una secuencia de bytes consiste en un número entero no negativo llamado longitud y alguna función / correspondencia arbitraria que mapea cualquier número entero i que sea al menos cero y menor que la longitud a un valor de byte (un entero de 0 a 255).

Muchos de los objetos con los que trata en un programa típico no tienen esa forma, porque los objetos en realidad están compuestos de muchas asignaciones de memoria diferentes que están en diferentes lugares en la RAM, y podrían estar separados unos de otros por millones de bytes de cosas que no me importa Solo piense en una lista vinculada básica: cada nodo en la lista es una secuencia de bytes, sí, pero los nodos están en muchas ubicaciones diferentes en la memoria de su computadora, y están conectados con punteros. O simplemente piense en una estructura simple que tenga un puntero a una cadena de longitud variable.

La razón por la que queremos serializar estructuras de datos en una secuencia de bytes es generalmente porque queremos almacenarlos en el disco o enviarlos a un sistema diferente (por ejemplo, a través de la red). Si intenta almacenar un puntero en el disco o enviarlo a un sistema diferente, será bastante inútil porque el programa que lee ese puntero tendrá un conjunto diferente de áreas de memoria disponibles.


1
No estoy seguro de que sea una gran definición de una secuencia. La mayoría de las personas definirían una secuencia como, bueno, una secuencia: una línea de cosas una tras otra. Por su definición, int seq(int i) { if (0 <= i < length) return i+1; else return -1;}es una secuencia. Entonces, ¿cómo voy a almacenar eso en el disco?
David Richerby

1
Si la longitud es 4, almaceno un archivo de cuatro bytes con contenido: 1, 2, 3, 4.
David Grayson

1
@DavidRicherby Su definición es equivalente a "una línea de cosas una tras otra", es solo una definición más matemática y precisa que su definición intuitiva. Tenga en cuenta que su función no es una secuencia porque para tener una secuencia necesita esa función y otro número entero que se llama longitud.
user253751

1
@FreshAir Mi punto es que la secuencia es 1, 2, 3, 4, 5. Lo que escribí es una función . Una función no es una secuencia.
David Richerby

1
Una forma sencilla de escribir una función en el disco es la que ya propuse: para cada entrada posible, almacene la salida. Creo que tal vez todavía no lo entiendes, pero no estoy seguro de qué decir. ¿Sabía que en los sistemas integrados es común convertir funciones costosas como sinen una tabla de búsqueda, que es una secuencia de números? ¿Sabía que su función es la misma que esta para las entradas que nos interesan? int seq(n) { int a[] = [1, 2, 3, 4]; return a[n]; } ¿Por qué dice exactamente que mi archivo de cuatro bytes es una representación inadecuada?
David Grayson

2

Las complejidades reflejan las complejidades de los datos y los objetos mismos. Estos objetos pueden ser objetos del mundo real u objetos de computadora solamente. La respuesta está en el nombre. La serialización es la representación lineal de objetos multidimensionales. Hay muchos problemas además de la RAM fragmentada.

Si puede aplanar 12 matrices de cinco dimensiones y algún código de programa, la serialización también le permite transferir un programa de computadora completo (y datos) entre máquinas. Los protocolos informáticos distribuidos, como RMI / CORBA, utilizan la serialización ampliamente para transferir datos y programas.

Considera tu factura telefónica. Puede ser un solo objeto, que consta de todas sus llamadas (lista de cadenas), monto a pagar (entero) y país. O su factura telefónica podría estar al revés de lo anterior y consistir en llamadas telefónicas detalladas y discretas vinculadas a su nombre. Cada aplanado se verá diferente, reflejará cómo su compañía telefónica escribió esa versión de su software y la razón por la cual las bases de datos orientadas a objetos nunca despegaron.

Es posible que algunas partes de una estructura ni siquiera estén en la memoria. Si tiene un almacenamiento en caché diferido, algunas partes de un objeto solo pueden referenciarse a un archivo de disco y solo se cargan cuando se accede a esa parte de ese objeto en particular. Esto es común en los marcos de persistencia graves. Los BLOB son un buen ejemplo. Getty Images podría almacenar una enorme imagen de varios megabytes de Fidel Castro y algunos metadatos como el nombre de la imagen, el costo del alquiler y la imagen misma. Es posible que no desee cargar la imagen de 200 MB en la memoria cada vez, a menos que realmente lo mire. Serializado, todo el archivo requeriría más de 200 MB de almacenamiento.

Algunos objetos ni siquiera pueden ser serializados en absoluto. En la tierra de la programación Java, puede tener un objeto de programación que represente la pantalla de gráficos o un puerto serie físico. No hay un concepto real de serializar ninguno de ellos. ¿Cómo enviarías tu puerto a otra persona a través de una red?

Algunas cosas como contraseñas / claves de cifrado no deben almacenarse ni transmitirse. Se pueden etiquetar como tales (volátiles / transitorios, etc.) y el proceso de serialización los omitirá, pero pueden vivir en la RAM. Omitir estas etiquetas es cómo las claves de cifrado se envían / ​​almacenan inadvertidamente en ASCII simple.

Esta y las otras respuestas es la razón por la cual es complicado.


2

El problema que tengo es: ¿no están todas las variables (ya sean primitivas como int u objetos compuestos) ya representadas por una secuencia de bytes?

Sí lo son. El problema aquí es el diseño de esos bytes. Un simple intpuede tener 2, 4 u 8 bits de largo. Puede estar en endian grande o pequeño. Puede estar sin firmar, firmado con el complemento de 1 o incluso en alguna codificación de bits súper exótica como negabinary.

Si simplemente descarga el intarchivo binario de la memoria y lo llama "serializado", debe conectar prácticamente toda la computadora, el sistema operativo y su programa para que sea deserializable. O al menos, una descripción precisa de ellos.

Entonces, ¿qué hace que la serialización sea un tema tan profundo? Para serializar una variable, ¿no podemos simplemente tomar estos bytes en la memoria y escribirlos en un archivo? ¿Qué complejidades me he perdido?

La serialización de un objeto simple consiste en escribirlo de acuerdo con algunas reglas. Esas reglas son muchas y no siempre son obvias. Por ejemplo, un xs:integeren XML está escrito en base-10. No es base 16, no es base 9, sino 10. No es una suposición oculta, es una regla real. Y tales reglas hacen que la serialización sea una serialización. Porque, prácticamente, no hay reglas sobre el diseño de bits de su programa en la memoria .

Eso fue solo la punta de un iceberg. Tomemos un ejemplo de una secuencia de esas primitivas más simples: un C struct. Se podría pensar que

struct {
short width;
short height;
long count;
}

tiene un diseño de memoria definido en una computadora determinada + SO? Pues no. Dependiendo de la #pragma packconfiguración actual , el compilador rellenará los campos. En la configuración predeterminada de la compilación de 32 bits, ambos shortsse rellenarán a 4 bytes, por lo structque en realidad tendrán 3 campos de 4 bytes en la memoria. Entonces, ahora, no solo tiene que especificar que shorttiene 16 bits de longitud, es un número entero, escrito en complemento negativo de 1, endian grande o pequeño. También debe escribir la configuración de empaque de estructura con la que se compiló su programa.

De eso se trata más o menos la serialización: hacer un conjunto de reglas y apegarse a ellas.

Esas reglas se pueden expandir para aceptar estructuras aún más sofisticadas (como listas de longitud variable o datos no lineales), características adicionales como legibilidad humana, versiones, compatibilidad con versiones anteriores y corrección de errores, etc. Pero incluso escribir una sola intya es bastante complicado si usted solo quiero asegurarme de que puedas volver a leerlo de manera confiable.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.