CodeGym /Cursos /C# SELF /Profundizando en el formato binario y sus problemas

Profundizando en el formato binario y sus problemas

C# SELF
Nivel 43, Lección 3
Disponible

1. Introducción

En resumen: el formato binario de serialización convierte un objeto en una secuencia de bytes que codifican su estructura y valores de la forma más compacta posible. Imagina que no sólo describes el objeto con palabras (como en JSON o XML), sino que escribes cada bit tal y como se guarda en memoria.

En formato de texto los datos son como una carta a un amigo en ruso (cada carácter es comprensible por una persona). En formato binario es más bien código Morse, donde cada punto y raya se registra de la forma más corta y leerlo "a mano" es imposible.

Esquema: comparación de formatos

Formato Legible por humanos Tamaño del archivo Velocidad (escritura/lectura) Compatibilidad
XML/JSON Sí Grande Más lento Buena
Binario No Pequeño Muy rápido Limitada

¿Cómo funciona la serialización binaria en .NET?

En el ecosistema .NET históricamente hubo una herramienta principal para la serialización binaria — la clase BinaryFormatter. Pero con la evolución de la plataforma se la consideró insegura y fue eliminada de .NET 9. Actualmente los estándares son otros: BinaryWriter/BinaryReader, y para objetos complejos — librerías externas (por ejemplo, protobuf-net).

Brevísimo repaso histórico

BinaryFormatter podía tomar cualquier clase marcada con el atributo [Serializable] y convertirla en bytes, y al deserializar reconstruir la estructura del objeto. Suena mágico, pero ahí residían muchos problemas (de eso hablaremos más abajo).

Herramientas modernas

Para tipos primitivos y estructuras simples es cómodo usar las clases BinaryWriter y BinaryReader. Para objetos complejos hay librerías de terceros (por ejemplo, protobuf-net, MessagePack-CSharp, etc.).

2. Serialización de primitivos con BinaryWriter

Sigamos mejorando nuestra aplicación de estudio. Por ejemplo, queremos guardar la configuración del usuario (nombre, puntuación, tiempo de inicio de sesión) en un archivo binario.

public class UserProfile
{
    public string Name { get; set; }
    public int Score { get; set; }
    public DateTime LoginTime { get; set; }
}

public static Task SaveUserProfile(UserProfile profile, string filePath)
{
    // Abrimos el archivo para escritura
    using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
    using var writer = new BinaryWriter(stream);

    // Escribimos los datos por partes. Primero la cadena, luego el número, luego la fecha
    writer.Write(profile.Name ?? string.Empty); // cadena
    writer.Write(profile.Score);                // entero
    writer.Write(profile.LoginTime.ToBinary()); // la fecha se convierte a "long"
}

Ahora un ejemplo de lectura:

public static Task<UserProfile> LoadUserProfile(string filePath)
{
    using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
    using var reader = new BinaryReader(stream);

    string name = reader.ReadString();
    int score = reader.ReadInt32();
    long dateData = reader.ReadInt64();
    DateTime loginTime = DateTime.FromBinary(dateData);

    return new UserProfile { Name = name, Score = score, LoginTime = loginTime };
}

Particularidades de la serialización binaria de primitivos

Con BinaryWriter serializamos cada propiedad por separado. Es un método fiable y predecible: si la estructura de datos cambia, se nota en el código.

3. Problemas de la serialización binaria clásica

Veamos ahora la otra cara de la moneda. ¿Por qué Microsoft criticó tan fuertemente BinaryFormatter e incluso prohibió su uso?

Fragilidad del esquema (Schema Evolution Hell)

Los datos binarios están fuertemente ligados a la estructura de la clase. Cambiaste la clase (renombraste un campo, añadiste uno nuevo, eliminaste otro) — los archivos binarios antiguos dejan de ser legibles. Cambiaste el orden de las propiedades — también es un problema.

Ilustración:

// Ayer
public class Profile
{
    public string Name;
    public int Score;
}

// Hoy
public class Profile
{
    public string Name;
    public double Rating; // Se añadió un nuevo campo
    public int Score;
}

Leer un archivo antiguo provocará o bien un error, o bien que los campos se interpreten con "datos mezclados". A diferencia de JSON o XML, donde los elementos faltantes se pueden omitir, el formato binario no se adapta a los cambios — es como una carretera de hormigón: te sales un poco y la bicicleta se cae.

Vulnerabilidades de deserialización

El mayor problema de BinaryFormatter es la potencial vulnerabilidad. Si tu software deserializa datos binarios recibidos de una fuente no confiable (por ejemplo, un usuario por Internet), un atacante puede inyectar un "objeto" malicioso. En el pasado esto llegó incluso a ejecución remota de código en la máquina víctima.

Portabilidad y compatibilidad

Un serializador binario está muy ligado a la representación interna de los datos en .NET y a la versión del runtime, del compilador y la arquitectura (por ejemplo, x64/ARM). Si serializas en Windows y tratas de deserializar en Linux — ¡sorpresas garantizadas! Incluso entre versiones de .NET pueden surgir incompatibilidades.

Incomodidad para diagnosticar

Con formatos de texto, si surge un problema puedes abrir el archivo, mirar el contenido y deducir qué falló. Un archivo binario es un misterio sellado. Lo único que tus ojos ven es un flujo de bytes sin sentido. "Analizar" ese archivo es un hobby para valientes.

4. Serialización binaria de objetos complejos

Referencias

BinaryFormatter recordaba las relaciones entre objetos (por ejemplo, si dos propiedades apuntaban al mismo objeto), pero BinaryWriter y la mayoría de librerías externas no hacen esa magia. Normalmente la serialización es por composición: incluido un objeto dentro de otro y escritos en orden.

Referencias cíclicas

Serializar objetos con referencias cíclicas (por ejemplo, la "madre" tiene la propiedad Child, y el "hijo" tiene la propiedad Parent que apunta al padre) o bien lanza un error, o bien causa un bucle infinito.

Ejemplo:

public class Node
{
    public Node? Next { get; set; }
    public Node? Prev { get; set; }
}

Intentar serializar este objeto "de forma naïve" llevará a un ciclo infinito.

5. Serialización binaria y portabilidad

Cualquier formato binario (especialmente uno hecho a mano) es un formato "solo para nosotros". Si vas a intercambiar datos con otros programas o guardarlos "por décadas" — elige estándares abiertos: JSON, XML o ProtoBuf.

¿Cuándo tiene sentido la serialización binaria?

  • Si los datos viven dentro de una sola aplicación y se guardan "a corto plazo".
  • Si la velocidad y la compacidad son importantes (por ejemplo, para logs grandes o intercambio entre servicios dentro de la misma ecosistema).
  • Si controlas estrictamente ambos extremos: tanto la serialización como la deserialización.

Alternativas: protobuf, MessagePack y otras

  • protobuf-net: puerto de Google Protocol Buffers para .NET, adecuado para intercambios multiplataforma y compatibilidad.
  • MessagePack-CSharp: implementación rápida de MessagePack para .NET.

A diferencia del "crudo" BinaryWriter, estas librerías implementan esquemas, soportan evolución del formato, portabilidad y seguridad. Úsalas si planeas alguna compatibilidad con otros sistemas.

6. Serialización binaria "manual"

Si aun así necesitas escribir datos binarios (por ejemplo, en apps donde la performance importa), usa BinaryWriter/BinaryReader — y siempre codifica explícitamente el orden y el tipo de los datos.

Consejos:

  • Siempre escribe los datos en el mismo orden en que planeas leerlos.
  • Al cambiar la estructura del archivo mantén un número de versión o escribe una "cabecera mágica" (Magic Header).
  • Añade la longitud de cadenas/arrays antes de escribir los datos en sí.
  • Documenta la estructura del archivo: si no, dentro de un año ni tú entenderás tu propio formato.

Ejemplo: versionado

// Guardamos el número de versión del formato en la primera posición
writer.Write((byte)1); // Versión 1

writer.Write(profile.Name ?? "");
writer.Write(profile.Score);
writer.Write(profile.LoginTime.ToBinary());

/*
Permite, al cambiar el formato posteriormente, añadir condiciones de lectura
*/

7. Errores típicos al trabajar con serialización binaria

Codificaste los campos en un orden y al leerlos los cambiaste. Como resultado, los valores "se descolocan": una cadena se lee como int, un int como fecha, etc.

Escribiste 10 objetos, y lees 11. Se rompe el flujo: se lanzará una excepción por fin de archivo.

Cambiaste la estructura de la clase y los archivos binarios antiguos no se pueden leer — se pierde todo el historial de datos.

Olvidaste manejar excepciones al leer un archivo importante — la aplicación se cae ante el primer fallo en disco (por ejemplo, EndOfStreamException).

Intentas intercambiar archivos binarios entre distintos lenguajes sin un formato explícito — en el 99% de los casos eso será un dolor asegurado.

Deserializas datos recibidos por la red de usuarios desconocidos — hola, vulnerabilidades. Nunca uses BinaryFormatter; valida la entrada y usa formatos seguros.

2
Tarea
C# SELF, nivel 43, lección 3
Bloqueada
Escritura de datos de usuario en un archivo binario
Escritura de datos de usuario en un archivo binario
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION