Ejemplos java y C/linux

Tutoriales

Enlaces

Licencia

Creative Commons License
Esta obra está bajo una licencia de Creative Commons.
Para reconocer la autoría debes poner el enlace http://www.chuidiang.com

Mensajes entre sockets

En los ejemplos hasta ahora (ejemplo simple, ejemplo con select) el servidor y el cliente se han pasado simples enteros o caracteres de uno a otro. Esto para una aplicación real es demasiado simple. Lo normal es que entre un cliente y un servidor se intercambien información más compleja, estructuras de datos completas. Vamos a ver cuales son los mecanismos habituales para esta transmisión de mensajes entre sockets.

LAS ESTRUCTURAS DE DATOS

Supongamos, por ejemplo, que el cliente puede enviar al servidor los siguientes mensajes:

El servidor, como respuesta, podrá enviar al cliente los mensajes: Lo primero que hay que hacer es escribir en un fichero .h todas estas estructuras/mensajes. Podemos, si queremos ser más ordenados o si hay muchos mensajes, escribir dos ficheros .h. En uno irán las estructuras de datos que van del cliente al servidor y en otro las que van del servidor al cliente. Puesto que en este caso es muy sencillo, sólo hay tres estructuras de datos, lo haremos todo junto.

/* No hay estructura de datos para que el cliente pida al servidor la fecha/hora */

/* Estructura de datos para que el cliente pida al servidor el día de la semana */
typedef struct MensajeDameFecha
{
    long fechaHora;
} MensajePedirFecha;

/* Estructura de datos que devuelve el servidor al cliente cuando se le pide la fecha/hora */
typedef estruct MensajeTomaFecha
{
   long fechaHora;
} MensajeTomaFecha;

/* Estructura de datos que devuelve el servidor al cliente cuando se le pide el día de la semana */
typedef struct MensajeTomaDiaSemana
{
    char diaSemana[12];
} MensajeTomaDiaSemana;

Un par de pequeños detalles.

LA CABECERA DEL MENSAJE

El primer problema que encontramos, cuando hay varios mensajes es cómo identificar qué mensaje vamos a recibir, cuántos bytes tenemos que leer. Por ello, antes de enviar un mensaje por un socket, se suele enviar una estructura común que hace de "cabecera" del mensaje. En esa cabecera va la información necesaria para identificar el mensaje (que se suele llamar "cuerpo") que se va a enviar después. El que recibe el mensaje, primero lee la cabecera y cuando sabe qué mensaje es el que va detrás, lo lee.

 ¿Qué contiene una cabecera?.

El único campo imprescindible para la cabecera es un identificador del mensaje que va detrás. Habitualmente suele ser un entero, de forma que si su valor es 0, el mensaje que va detrás es uno determinado, si es 1 es otro y así sucesivamente.

typedef struct Cabecera
{
    int identificador;
} Cabecera;

Es habitual también hacer que este entero sea un enumerado. Cada valor del enumerado es un identificador de un mensaje distinto. Igual que antes, se puede hacer un único enumerado para todos los mensajes o bien dos enumerados, de forma que en el primero van los identificadores de los mensajes que van del cliente al servidor, y en el segundo los del servidor al cliente. Hay que poner enumerado también para los mensajes que no llevan datos.

typedef Identificadores enum
{
    IdDameFecha,
    IdDameDiaSemana,
    IdTomaFecha,
    IdTomaDiaSemana
} Identificadores;

Para enviar el mensaje, por ejemplo, con la función write() se haría lo siguiente

MensajeDameDiaSemana mensaje;
Cabecera cabecera;

mensaje = ...; /* Se rellenan los datos del mensaje */
cabecera.identificador = IdDameDiaSemana; /* Se rellena la cabecera */

/* Se envía primero la cabecera y luego el mensaje */
write (socket, &cabecera, sizeof(cabecera));
write (socket, &mensaje, sizeof(mensaje));

La lectura es algo más compleja. Si leemos con la función read(), nos quedaría algo así como

Cabecera cabecera;

/* Leemos la cebecera */
read (scket, &cabecera, sizeof(cabecera));

/* En función de la cabecera leida, leemos el mensaje que va a continuación */
switch (cabecera.identificador)
{
    case IdDameFecha:
    {
        /* No hay que leer mas */
        ...  /* tratamiento del mensaje */
        break;
    }

    case IdDameDiaSemana:
    {
        MensajeDameDiaSemana mensaje; /* Se declara una variable del mensaje que queremos leer */
        read (socket, &mensaje, sizeof(mensaje)); /* Se lee */
        .... /* tratamiento del mensaje */
        break;
    }
}

Vemos que es necesario un switch o similar, de forma que cada case lee y trata uno de los posibles mensajes.
 

IDEAS PARA UNA PEQUEÑA LIBRERÍA

Puesto que el escribir cabecera y enviar mensajes son unas cuantas líneas de código que deberán realizarse con frecuencia, no es mala idea hacer una función que nos facilite la tarea y que, de paso, nos "oculte" la estructura exacta de la cabecera. Por ejemplo, el prototipo de la función podría ser

void escribeMensaje (int socket, int idMensaje, char *mensaje, int tamanho);

de forma que socket es el socket por el que queremos enviar el mensaje, idMensaje es el entero identificador del mensaje, mensaje es un puntero a la estructura del mensaje y tamanho es el tamaño de dicha estructura-mensaje. El código de la función, en forma simple, pordría ser:

void escribeMensaje (int socket, int idMensaje, char *mensaje, int tamanho)
{
    /* Se declara y rellena la cabecera */
    Cabecera cabecera;
    cabecera.identificador = idMensaje;

    /* Se envía la cabecera */
    write (socket, &cabecera, sizeof(cabecera);

    /* Si el mensaje no tiene cuerpo, hemos terminado */
    if ((mensaje == NULL) || (tamanho == 0))
        return;

    /* Se envía el cuerpo */
    write (socket, mensaje, tamnho);
}

    Una vez hecha la función, su uso es simple:

/* Se declara y rellena el mensaje que queremos enviar */
MensajeDameDiaSemana mensaje;
mensaje = ...;

/* Se envía llamando a nuestra función de librería */
escribeMensaje (socket, IdDameDiaSemana, (char *)&mensaje, sizeof(mensaje));

Para la lectura podemos hacer algo similar. El prototipo de la función sería:

void leeMensaje (int socket, int *idMensaje, char **mensaje);

 socket es el socket por el que queremos leer. idMensaje es un puntero a entero. La función nos lo devolverá relleno con el identificador del mensaje que ha recibido. mensaje es un puntero a un puntero a char (para ver por qué, mira en punteros como parámetros). La función creará la memoria necesaria para leer el mensaje y nos la devolverá como puntero a char. Nosotros, fuera de la función, haremos el cast adecuado en función del idMensaje devuelto y liberaremos la memoria cuando no la necesitemos.

Si nos ponemos a hacer el código, tendremos algo como esto

void leeMensaje (int socket, int *idMensaje, char **mensaje)
{
    Cabecera cabecera;

    /* Se lee la cabecera */
    read (socket, &cabecera, sizeof(cabecera));

    switch (cabecera.identificador)
    {
        case ...  /* creo que hay un problema... */
    }
}

Pues parece que tenemos un problema. Si empezamos a hacer los case, nuestra función va a depender de la mensajería específica de nuestra aplicación. Deberemos rehacer esta función cada vez que hagamos una aplicación con mensajes distintos o cada vez que decidamos modificar un mensaje o hacer uno nuevo. Esto no es muy adecuado para una librería de funciones que queramos que sea más o menos general.

Para resolver este problema, tenemos necesidad de añadir un nuevo campo a la estructura Cabecera: un campo que indique la longitud del mensaje.

typedef struct Cabecera
{
    int identificador;
    int longitud; /* Longitud del mensaje, en bytes */
} Cabecera;

Con esto, nuestra función de escribir no se ve afectada (únicamente hay que rellenar el nuevo campo con el parámetro tamanho antes de enviar la cabecera). Sin embargo, se nos facilita enormemente la función de leer. Después de leer la cabecera, hay que crear un "buffer" del tamaño que indique el campo longitud y leer en ese buffer la estructura, sin necesidad de saber qué estructura es.

void leeMensaje (int socket, int *idMensaje, char **mensaje)
{
    Cabecera cabecera;
    *mensaje = NULL;  /* Ponemos el mensaje a NULL por defecto */

    read (socket, &cabecera, sizeof(cabecera)); /* Se lee la cabecera */

    /* Rellenamos el identificador para devolverlo */
    *idMensaje = cabecera.identificador;

    /* Si hay que leer una estructura detrás */
    if (cabecera.longitud > 0)
    {
        *mensaje = (char *)malloc (longitud);  /* Se reserva espacio para leer el mensaje */
        read (socket, *mensaje, longtud);
    }
}

Con la función hecha así, su utilización sería parecida a esto

char *mensaje = NULL;
int identificador;

/* Se lee el mensaje */
leeMensaje (socket, &identificador, &mensaje);

/* Se hace el tratamiento del mensaje según el identificador */
switch (identificador)
{
    case IdDameFecha:
    {
        ... /* tratamiento del IdDameFecha. No hay mensaje asociado */
        break;
    }

    case IdDameDiaSemana:
    {
        /* Se hace un cast de char * a MensajeDameDiaSemana *, para tener más accesibles los datos recibidos */
        MensajeDameDiaSemana *mensajeDiaSemana = NULL;
        mensajeDiaSemana = (MensajeDameDiaSemana *)mensaje;
        ... /* tratamiento de mensajeDiaSemana */
        break;
    }
}

/* Se libera el mensaje  cuando ya no lo necesitamos */
if (mensaje != NULL)
{
    free (mensaje);
    mensaje = NULL;
}

Vemos que no nos libramos del switch-case, pero en este caso queda en la parte de la aplicación y no en la función de la librería general. Por otra parte es lo lógico, puesto que la aplicación deberá tratar de distinta manera los mensajes.
 

MAS CAMPOS PARA CABECERA

Con los campos indicados de identificador y longitud tenemos más que suficiente. Sin embargo, hay aplicaciones que añaden más campos informativos a la cabecera. No vamos a entrar en detalle, pero algunos de ellos pueden ser:

CONCLUSIONES

Con esto queda explicado el cómo enviar mensajes complejos por los sockets e incluso se abre la posibilidad de incrementar las funciones para nuestra librería de sockets.

Es más, estas dos funciones son muy generales, puesto que no tienen nada que ver con sockets. El parámetro socket que se les pasa es en realidad un entero que representa un descriptor de fichero válido (un socket, un puerto serie o un fichero). Se podrían poner estas funciones en una librería separada y usarlas para otros tipos de comunicación o incluso para escribir en fichero estructuras de datos distintas (cada una con su cabecera). Para leer dicho fichero, se leería cabecera, estructura, cabecera, estructura y así sucesivamente.

Cómo hacer una librería.

Estadísticas y comentarios

Numero de visitas desde el 4 Feb 2007: