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.
Supongamos, por ejemplo, que el cliente puede enviar al servidor los siguientes mensajes:
/* 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.
typedef struct UnaEstructura
{
char caracter;
int numero;
} UnaEstructura;
printf ("El tamaño es %d\n", sizeof(UnaEstructura));
asombrosamente nos saldrá 8. El campo caracter ocupa 1 byte, numero
ocupa 4 bytes. Los otros 3 que faltan los ha metido el compilador entre
caracter y numero para hacer que todo cuadre con múltiplos de 4
bytes. Cuando enviemos la estructura por el socket, estaremos enviando
los 8 bytes. Por ello suele ser conveniente o bien ser consciente de esto
o bien hacer que nuestras estructuras/campos sean múltiplos exactos
de 4.
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.
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.
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:
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.