jueves, 4 de enero de 2018

C++ - Deserialización de clases

Deserialización de clases

En esta entrada vamos a terminar lo empezado en esta otra: Serialización de clases


Lo primero: Aquí voy a completar la clase creada en la otra entrada. Doy por supuesto que se sabe lo explicado en la entrada anterior.

Recordemos el código final de la entrada anterior:

class Prueba{
public:
    int n;
    string s;
    
    string serializar(){
        string t;
    
        // int n
        t.append((char*)&n, sizeof(n));
    
        // string s
        unsigned int size = s.size();
        t.append((char*)&size, sizeof(size));
        t.append(s.c_str(), size);
    
        return t;
    }
};

La idea ahora es deserializarlo, es decir, recuperar los datos guardados (serializados) en una string (la generada por el método "serializar").

Lo primero, veamos un ejemplo de main que utilizaremos para probar el correcto funcionamiento de ambas funciones:

int main() {
    Prueba prueba;

    prueba.n = 66;
    prueba.s = "Code0x66";

    String objetoSerializado = prueba.serializar();

    prueba.n = 0;
    prueba.s = "";

    prueba.deserializar(objetoSerializado);

    cout << prueba.n << '\n' << prueba.s << endl;
}

En orden:
  1. Guardamos datos en el objeto "prueba"
  2. Serializamos el objeto y guardamos el resultado en la variable "objetoSerializado"
  3. Cambiamos los valores del objeto para asegurarnos de que la deserialización es correcta
  4. Deserializamos a partir de la string almacenada en la variable, y mostramos los valores para corroborar su funcionamiento
 Ahora hablemos del método "deserializar". Qué parámetros tendrá, qué retornará, cómo será...
Lo primero es que una deserialización puede ser correcta o no. En caso de que no lo sea, podemos informar lanzando una excepción, o, como haremos ahora, retornando un bool, que será "false" en caso de que hubiera error. (Un error se genera si la string a deserializar no tiene un formato correcto)

Una de las cosas más tediosas de la deserialización es aplacar errores y verificar la string. Esto lo haremos de último, para ver los problemas que puede dar.

El método recibirá como parámetro la string a deserializar, y retornará un bool indicando si ha deserializado correctamente.

Empecemos. Observando el método "serializar", podemos ver que genera una cadena con este formato (en binario):
[n (int)][s.size() (unsigned int)][s (char...)]

Lo primero, el orden. En el mismo orden que serializamos, deserializamos. Así que empezaremos por la variable "n", que es int.
En el comienzo del método, guardaremos en un char* la cadena, ya que será más cómodo trabajar así con ella.

bool deserializar(const string& str){
    const char* cadena = str.c_str();
    int indice = 0; // Indice de la cadena por el que vamos

    // Haciendo un cast de char* a int*, interpretamos la memoria como int
    // Con lo cual, al desreferenciarla (*), obtenemos el int tal y como lo guardamos
    int nuevaN = * (int*) cadena;

    // Incrementamos el indice de la cadena
    indice += sizeof(int);

    // Aquí ya sumamos el indice a la cadena, para saltar lo ya leido
    // Podría haberse hecho la primera vez también
    // Pero es superfluo y lo dejo asi como ejemplo
    unsigned int sizeS = * (unsigned int*) (cadena + indice);
    indice += sizeof(unsigned int);

    // Aqui toca leer  caracteres. Pero aprovecharemos que tenemos la string
    // con su metodo substr(indiceInicial, cantidad)
    string nuevaS = str.substr(indice, sizeS);

    // Por ultimo, asignamos las variables
    s = nuevaS;
    n = nuevaN;
}

Ya está el código explicado. Comentemos algunos detalles, empezando por lo último. La razón de que se asignen los campos del objeto al final es simple: Asegurar la integridad del objeto. O se deserializa todo, o no se deserializa nada. En caso de error, el objeto estará exactamente igual que antes.

Y hablando de errores, veréis que no he puesto ningún return, y es una función bool.
¿Qué clase de errores se pueden generar aquí? Por orden de aparición:
  1. El primer int son sizeof(int) bytes (generalmente, 4 char). Habría error si la cadena a deserializar tuviera menos de 4 caracteres (estaríamos leyendo memoria fuera de la cadena).
  2. Lo mismo con el tamaño del campo "s", solo que acumulamos tamaño. 4 anteriores más 4 bytes del unsigned int hacen 8 caracteres. Si la string tiene menos de 8 caracteres, habrá un error. Esto lo podemos comprobar ya al principio, y obviamos la primera comprobación. 8 es el tamaño mínimo de la cadena. Ahora, veamos el tamaño justo.
  3. Cuando leamos el tamaño de la cadena, sabremos el tamaño total exacto de la string a deserializar, que será 4 + 4 + tamañoCadena. Lo único a remarcar, y no menos importante, es que esta comprobación a de ir exactamente en el momento en que tengamos los datos necesarios (el tamaño de la cadena).
Bien pues, veamos como quedaría si retornamos "false" cuando la cadena no tiene el tamaño mínimo (sin los comentarios de antes):

bool deserializar(const string& str){
    const char* cadena = str.c_str();
    int indice = 0;

    // Primera condición
    if(str.size() < sizeof(int) + sizeof(unsigned int)){
        return false;
    }
    
    int nuevaN = * (int*) cadena;
    indice += sizeof(int);
    
    unsigned int sizeS = * (unsigned int*) (cadena + indice);
    indice += sizeof(unsigned int);
    
    // Segunda condición
    if(str.size() < sizeof(int) + sizeof(unsigned int) + sizeS){
        return false;
    }

    string nuevaS = str.substr(indice, sizeS);

    s = nuevaS;
    n = nuevaN;

    // No olvidar el retorno "true" por defecto
    return true;
}

Con esto, queda casi listo. Queda hacer el código para probarlo. He aquí el main:

int main() {
    Prueba prueba;

    prueba.n = 66;
    prueba.s = "Code0x66";

    string objetoSerializado = prueba.serializar();

    prueba.n = 0;
    prueba.s = "";

    if(prueba.deserializar(objetoSerializado)){
        cout << prueba.n << '\n' << prueba.s << endl;
    }else{
        cout << "Error deserializando" << endl;
    }
}

Podéis probar el retorno false dándole una cadena incorrecta, por ejemplo: prueba.deserializar("test");

Un detalle más. También podéis retornar false en caso de que la cadena no tenga exactamente el tamaño que debe tener, es decir, que es más grande de lo que vosotros vais a consumir. Eso podría hacerse cambiando el operador "<" del último if por el operador "!=". Si es diferente al tamaño que pedimos, error: if(str.size() != sizeof(int) + sizeof(unsigned int) + sizeS)
Para probarlo, basta hacer: prueba.deserializar(objetoSerializado + "datos extra")

Y con esto, queda lista la deserialización. Es un proceso más lento y tedioso que la serialización en cuanto a tiempo programando, principalmente por todas las comprobaciones que haremos, pero es un proceso necesario.

Por último, y para despedir esta entrada, hay otra forma de hacer un método de deserialización. Deserializar no necesita que exista un objeto; deserializar va a generar un objeto a partir de una cadena (en este ejemplo, realmente estamos generando otro objeto y poniéndolo encima del que ya teníamos). Así pues, otra forma de hacerlo, sería con un método static. La forma de llamarlo podría ser esta, tirando excepción si falla (no lo voy a definir, eso es cosa del que lo quiera hacer):
Prueba prueba = Prueba::deserializar(objetoSerializado);

Y ahora sí, cierro entrada. Hace tiempo que tenía esta entrada pendiente; fue gracias a un comentario que lo recordé ;)

Un saludo, hasta próximas entradas!

No hay comentarios:

Publicar un comentario