En la entrada pasada describí (de forma relativamente informal) el modelo cliente-servidor y las funciones involucradas en la programación de Sockets bajo Linux, tanto en el caso de sockets orientados a conexión, como en el caso de sockets no orientados a conexión.
En esta entrada voy a hacer una presentación un poco más formal y voy a desarrollar un ejemplo de socket utilizando UDP. Lo aquí mostrado está basado en este tutorial y los apuntes de mis clases de Redes.
Comenzamos por describir los archivos de cabecera involucrados:
1) types.h
Contiene las definiciones de una gran cantidad de tipos de datos utilizados en llamadas al sistema, en particular, algunos utilizados en los siguientes archivos de cabecera.
2) socket.h
Incluye todas las estructuras de datos necesarias para el trabajo con sockets.
3) in.h
Contiene constantes y estructuras necesarias para trabajar con direcciones de Internet.
Entre las estructuras de datos utilizadas, es destacable sockaddr_in, cuya definición es la siguiente:
struct sockaddr_in
{
short sin_family; /* must be AF_INET */
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8]; /* Not used, must be zero */
};
Los objetos tipo sockaddr_in se utilizan par almacenar direcciones de Internet. Como veremos en el código, necesitaremos lógicamente 2 variables de este tipo: una para almacenar la dirección del servidor y una para la dirección del cliente.
El caso de estudio
Tenemos un sistema de balanceo de carga, en el que por X razón, nos interesa que el nodo director conozca en todo momento el estado de carga de los servidores reales. En una entrada pasada, estudiamos el método utilizado para el cálculo de la carga del sistema en Linux, cuyo valor podemos leer en el archivo /proc/loadavg.
Con esto en mente, lo que planeo hacer a continuación es:
1) Un proceso para los servidores reales que cada cierto tiempo envíe a través de un socket UDP su carga al nodo director.
2) Un proceso en el nodo director que esté siempre en espera de la data enviada por los servidores reales y la almacene en un log.
Procedimiento para los servidores reales
Este procedimiento es realmente sencillo, básicamente hace 3 cosas:
1) Abrir el socket UDP
2) En un proceso de n iteraciones lee el archivo /proc/loadavg y envía lo leído a través del socket
3) Envía una señal de parada al nodo remoto y cierra el socket
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#define tam_buffer 100
#define NAME_FILE "///proc//loadavg"
int main( int argc, char *argv[] )
{
FILE *file_load;
char *d, line_load[tam_buffer];
double OneMin, FiveMins, FifteenMins;
int i = 0, socket_udp, tam, num_bytes, time_sleep;
struct sockaddr_in addressip;
int n_packets = 0, curr_packets = 0;
/* validation input arguments */
if( argc != 5 )
{
printf(">:ERROR: Use mode: ./realsv IP PORT TIME N_PACKETS\n");
exit( -1 );
}
time_sleep = atoi(argv[3]);
n_packets = atoi(argv[4]);
/* init socket */
addressip.sin_family = AF_INET;
addressip.sin_port = htons( atoi( argv[2] ) );
inet_aton( argv[1], &(addressip.sin_addr) );
bzero( &(addressip.sin_zero), 8 );
socket_udp = socket(AF_INET, SOCK_DGRAM, 0);
tam = sizeof(struct sockaddr);
do
{
if((file_load = fopen(NAME_FILE,"r")) == NULL )
{
fprintf(stderr,"Can't open the file, %s \n",NAME_FILE);
exit(1);
}
else
{
if(feof(file_load))
{
fclose (file_load);
break;
}
d = fgets(line_load,100,file_load);
sscanf(line_load,"%lf %lf %lf", &OneMin, &FiveMins,
&FifteenMins);
// Send load to lvs
num_bytes = sendto(socket_udp, (double *)&OneMin,
sizeof( OneMin ), 0,
(struct sockaddr *)&addressip, tam );
i++;
printf("\n>: Send No: %d", i);
printf("\n>: bytes send: %d", num_bytes);
printf("\n>: load last minute: %f \n",OneMin);
// Close the file
fclose (file_load);
sleep(time_sleep);
}
curr_packets++;
}while(curr_packets < n_packets);
// Send STOP Signal:
OneMin = -1.0;
num_bytes = sendto(socket_udp, (double *)&OneMin,
sizeof( OneMin ), 0,
(struct sockaddr *)&addressip, tam );
// Close the connection:
printf("\n>: Close conection.");
close(socket_udp);
return 0;
}
El código se explica solo, sin embargo haré algunas notas.
- El procedimiento toma como argumentos: la IP del nodo remoto, el puerto del nodo remoto, el tiempo (en segundos) que indica cada cuanto se debe enviar un paquete y el número de paquetes que se deben enviar.
- Se utiliza la función sleep() como temporizador para el envío de los paquetes.
- Se utiliza AF_INET para indicar que se trata de un socket que funciona en red.
- Se utiliza la función htons() para evitar problemas "entendimiento" entre máquinas, pues todos los procesadores no representan los número de la misma forma (algunos lo hacen con el byte más significativo a la izquierda, otros a la derecha).
- Es importante comprender los argumentos que se pasan a sendto().
- Cuando ya se han enviado los n paquetes especificados por línea de comando, se rompe el bucle, se envía un paquete con -1 para que el nodo remoto (nodo director) sepa que ya hemos terminado, y finalmente se cierra el socket.
Procedimiento para el nodo director
Ahora continuaremos con el procedimiento que se debe ejecutar en el nodo director, el cual es tanto o más sencillo que el anterior. En el nodo director, simplemente estaremos a la espera de los datos enviados por el socket y mostraremos por pantalla cada vez que recibamos alguno.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define tam_load 8
#define tam_ip 20
int main(int argc,char *argv[])
{
struct sockaddr_in iprealsv;
char addressip[tam_ip];
double buffer;
int socket_udp, tam, numbytes = 1, n = 0, interval = 1;
/* validation input arguments */
if(argc != 3)
{
printf("\n>:ERROR: Use mode: ./lvsrcp PORT TIME \n");
exit( -1 );
}
/* init socket */
iprealsv.sin_family = AF_INET;
iprealsv.sin_port = htons( atoi(argv[1]) );
iprealsv.sin_addr.s_addr = INADDR_ANY;
bzero( &(iprealsv.sin_zero), 8 );
socket_udp = socket( AF_INET, SOCK_DGRAM, 0 );
tam = sizeof( struct sockaddr );
if ((bind( socket_udp, (struct sockaddr *)&iprealsv, tam )) == -1)
printf("\n>:ERROR: bind function \n");
while (1)
{
n++;
numbytes = recvfrom(socket_udp, (double *)&buffer,
sizeof(buffer), 0,
(struct sockaddr *)&iprealsv, (socklen_t *)&tam);
printf(">: Package received #: %d \n", n);
printf(">: bytes received: %d \n", numbytes);
printf(">: load real server: %f \n", buffer);
strcpy(addressip, inet_ntoa(iprealsv.sin_addr));
addressip[strlen(addressip)] = '\0';
printf(">: real_server: address IP :%s and load: %f \n", addressip, buffer);
if(buffer < 0)
break;
}
printf("\n>: Finished connection. \n");
/* close connection */
close( socket_udp );
return 0;
}
Muy sencillo el código. Simplemente abrimos el socket, tal como en el primer programa, y luego creamos un bucle infinito para esperar a que lleguen los datos. Cuando el programa recibe un paquete cuyo dato es -1, se rompe el bucle, se cierra el socket y se termina el programa.
Es destacable notar que ambas máquinas deben saber qué tipo de datos están enviando/recibiendo; en este caso el único dato que se está enviando es un valor real. Si el paquete está formado por varios datos, lógicamente tendremos que pasar por el proceso de armar el dato compuesto (seguramente en una cadena) del lado del cliente y luego desarmar el dato en el lado del servidor.