7.2. EL PROBLEMA DEL SERVIDOR DE SOCKETS.

  1. Introducción al problema.
  2. Ejemplo del Servidor de Sockets con procesos.
  3. Ejemplo del Servidor de Sockets con hilos.
  4. Comparación de las dos implementaciones.

Introducción al problema.

El problema del servidor de sockets es uno de los problemas más habituales que se presentan en la arquitectura cliente/servidor en redes. Representa un buen ejemplo de los problemas de comunicación entre procesos distribuidos.

El problema consiste en suponer que tenemos un aplicación servidor en un host, que se encarga de resolver las peticiones que le llegan de aplicaciones cliente que se encuentran en cualquier otro host conectado a la red o en el propio host local, devolviéndoles una determinada respuesta.

El problema se puede resolver de dos formas :

En el ejemplo que se presenta, se considera una red con protocolo TCP/IP. El servidor se arranca en un determinado puerto conocido y se bloquea a la espera de las peticiones de los clientes. Cuando llega una petición de un cliente, el servidor genera un descendiente que se encarga de procesar la petición y comunicarse con el cliente, mientras que el servidor padre se bloquea a la espera de una nueva petición. De esta forma se pueden servir varias peticiones de clientes de forma concurrente mientras se "escuchan" nuevas peticiones por el puerto de comunicación, lo cual garantiza una mejor respuesta del sistema.

Este problema es muy habitual en los sistemas. Un ejemplo lo podemos encontrar en el "daemon" httpd del CERN que es un servidor del protocolo HTTP, y es muy empleado en los sistemas que funcionan como servidores internet o intranet. En estos sistemas la carga casi exclusiva corresponde a peticiones de browsers clientes a dicho servidor httpd, por ello la reducción del tiempo de servicio de las peticiones incrementará el rendimiento del servidor de forma considerable.

Una posible solución al problema del servidor concurrente presentada en forma de seudocódigo sería la siguiente :

SERVIDOR :

- Inicialización.

- Crear socket de escucha en el puerto conocido.

- Repetir indefinidamente

- Esperar petición de cliente.

- Crear un proceso de SERVICIO de la petición.

SERVICIO :

- Recibir los datos correspondientes a la petición del cliente.

- Resolver la petición.

- Enviar la respuesta al cliente.

- Finalizar.

CLIENTE :

- Inicialización.

- Conectar con el servidor a través del puerto conocido.

- Enviar la petición al servidor.

- Esperar la respuesta del servidor.

- Cerrar la conexión con el servidor

- Finalizar.

Ejemplo del Servidor de Sockets con procesos.

El problema del servidor de sockets que se presenta a continuación es una particularización. La aplicación que se muestra como ejemplo consiste en dos programas : el servidor y el cliente. El objetivo es mostrar como se desarrolla el esquema básico de una aplicación cliente/servidor distribuida.

La implementación que se presenta se ha realizado para el sistema operativo Linux 1.2, y se han empleado procesos y primitivas de sincronización : mútex y variables de condición. Los mútex y variables de condición se han simulado mediante semáforos, al igual que en el ejemplo del productor/consumidor presentado anteriormente, con objeto de que el código fuente tenga el mayor parecido posible con la versión de hilos que se presenta posteriormente, y de esta forma poder mostrar las similitudes y diferencias entre la programación multiproceso y la programación multihilo. La comunicación entre cliente y servidor se realiza mediante tramas TCP.

El cliente obtiene un texto de la línea de argumentos, en la que también se puede indicar la dirección del host en la que se encuentra el servidor. Comprueba la existencia del servidor y establece la conexión con el servidor. A continuación envía una trama al servidor con la línea de texto, simulando la consulta al servidor. Después cierra la conexión con el servidor. Es un ejemplo muy sencillo pero que da una idea básica de la estructura de un cliente.

La sintaxis de ejecución del cliente es la siguiente :

cliente [-?|-h] <Texto> [<Dir_Servidor>] siendo

El código fuente para el cliente es el siguiente :

CLIENTE.C :

/****************************************************************************
   EJEMPLO DEL CLIENTE/SERVIDOR.
   Aplicacion:    cliente
   Version:       1.0
   Fecha: 27/11/1996
   Descripcion: Programa demostrativo del caso del cliente/servidor.
                El programa se conecta al servidor y le envia tramas.
                Si envia la trama 'FIN' el servidor finaliza.
                El cliente es valido tanto para la version multihilo como
                la version multiprocesos.
   Autor: Jose Maria Toribio Vicente
   Mail:  jmtoribi@poboxes.com
*****************************************************************************/
#define VERSION "cliente 1.0 - Cliente/Servidor\n"
#define SINTAX  "Sintaxis: %s [-?|-h] <texto> [<host_servidor>]\n"

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>

#include "tipos.h"
#include "tcp.h"
#include "getstats.h"
#include "tiempo.h"

/* PROGRAMA PRINCIPAL */
main(int argc, char **argv)
{
   char Texto[LONG_TEXTO+1];      /* Texto de la trama */
   char HostServidor[100];                /* Nombre del Host Servidor */
   int PuertoTCP;                 /* Puerto de conexion del Servidor */
   int SocketConsulta;            /* Socket de la consulta */
   estad_t estad[2];                  /* Datos estadisticos de la aplicacion */
   
#  ifdef EXEC_TIME   
   struct timeval t1,tp;          /* Tiempos de inicio y de programa */
   /* Obtener el tiempo actual */
   gettimeofday(&t1, NULL);
#  endif

   printf(VERSION);
   /* Comprobar si se solicita ayuda */
   if (argc > 1)
      if ( strcmp(argv[1],"-?") == 0 || strcmp(argv[1],"-h") == 0 ) {
         printf(SINTAX, argv[0]);
         exit(0);
      }
   /* Inicializa con valores por defecto */
   strcpy(HostServidor, HOST_SERVIDOR);
   PuertoTCP = PUERTO_TCP;

   /* Comprobar el numero de argumentos de la linea de comandos */
   switch(argc) {
      case 3:
         strcpy(HostServidor, argv[2]);
      case 2:
         if (strlen(argv[1]) > LONG_TEXTO) {
            printf("El texto '%s' es demasiado largo\n", argv[1]);
            exit(1);
         }
         strcpy(Texto, argv[1]);
         break;
      default:
         printf(SINTAX, argv[0]);
         exit(1);
   }
   /* Localizar si existe el Host */
   if ( !gethostbyname(HostServidor) ) {
      printf("El host '%s' no existe o no se puede localizar\n",
 HostServidor);
      exit(1);
   }

   /* Obtener los tiempos iniciales */
   getestad(&estad[0]);

   /* Abrir  Conexion al Servidor */
   SocketConsulta = OpenServidor(HostServidor, PuertoTCP);

   /* Realizar consulta */
   printf("Cliente enviando la Trama '%s'\n", Texto);
   EnviarTrama(SocketConsulta, strlen(Texto), Texto);

   /* Cerrar Conexion al Servidor */
   CloseServidor(SocketConsulta);

   /* Obtener tiempos finales */
   getestad(&estad[1]);

#  ifdef EXEC_TIME
   /* Obtener el tiempo actual y el tiempo de servicio */
   gettimeofday(&tp, NULL);
   DiffTime(&t1, &tp, &tp);   
   printf("Cliente: Tiempo de ejecucion = %d sg %d mms\n",
                  tp.tv_sec, tp.tv_usec);
#  endif

   /* Imprimir los tiempos */
   printestad(stderr, &estad[0], &estad[1]);

   /* Finaliza el programa */
   return(0);
}


El servidor, por su parte, se puede lanzar en background, y se le pueden pasar una serie de parámetros. El servidor emplea una zona de memoria compartida para almacenar cierta información sobre los servicios. Crea el socket de escucha, lo enlaza con un puerto determinado y se suspende a la espera de una petición. Cuando llega una petición, crea un proceso hijo mediante la sentencia fork() de UNIX que procesa la petición del cliente, heredando el socket de conexión con el cliente. El proceso de la petición se simula mediante un bucle contador iterativo, cuyo número de iteraciones se puede establecer al arrancar el servidor. El valor del número de iteraciones establecerá el tiempo de servicio. Además actualiza la variable de memoria compartida de tiempo de servicio. El proceso padre, por su parte, se bloquea de nuevo a la espera de una nueva petición. Cuando el proceso hijo que proporciona el servicio finaliza, termina sin que sea necesario que el padre capture su estado, ya que para evitar esto el hijo se creó desvinculado del padre.

Cuando el proceso de servicio recibe la trama FIN por primera vez, se lo indica al servidor a través de memoria global, de tal forma que en la siguiente petición FIN, el servidor está preparado para finalizar la escucha, cerrando el servicio, liberando las variables de sincronización y la memoria compartida. Por último imprime una serie de datos estadísticos a cerca del uso del servidor, como son el número de peticiones servidas, el tiempo total de servicio y el tiempo medio de servicio por petición.

Para los accesos a memoria compartida se emplea una variable de mútex, que garantiza el acceso exclusivo a los datos. Además se emplea una variable de condición para indicar la finalización del servidor.

La sintaxis de ejecución del servidor es la siguiente :

servidor [-?|-h] [num_iter [num_escuchas [num_peticiones]]] siendo

El código fuente para el servidor es el siguiente :

SERVIDOR.C :

/****************************************************************************
   EJEMPLO DEL CLIENTE/SERVIDOR.
   Aplicacion:    servidor
   Version:       1.0p
   Fecha: 27/11/1996
   Descripcion: Programa demostrativo del caso del cliente/servidor.
                Se implementa el Servidor como Multiproceso. El servidor
                recibe una peticion, y crea un proceso de servicio que gestiona
                dicha peticion.
                El programa emplea memoria compartida y semaforos compatibles
                con System V.
   Autor: Jose Maria Toribio Vicente
   Mail:  jmtoribi@poboxes.com
*****************************************************************************/
#define VERSION "servidor 1.0p - Cliente/Servidor (vers. procesos)\n"
#define SINTAX  "Sintaxis: %s [-?|-h] [num_iter [num_escuchas [num_peticiones]]]\n"

/* #define DEBUG */             /* Compilacion en modo depuracion */
/* #define EXEC_TIME */         /* Compilacion con tiempo de ejecucion */

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <sys/uio.h>
#include <unistd.h>

#include <signal.h>

#include "cond.h"
#include "tipos.h"
#include "tcp.h"
#include "getstats.h"
#include "tiempo.h"
#include "error.h"

/* Definicion de Identificadores de Claves de Recursos del Sistema */
#define MUTEX1_KEY      ((key_t)188761L)
#define COND1_KEY       ((key_t)188762L)
#define SHMEM_KEY       ((key_t)188763L)

/* Procedimiento del Servicio */
void *ProcesarPeticion(void *);

/* Estructura de datos empleada para comunicacion entre el servidor de
   escucha y los procesos de servicio */
typedef struct s_datos_t {
   mutex_t           lock;   /* Mutex de acceso a los datos de memoria global */
   cond_t    no_run;        /* Condicion de running */
   int               Finalizar;/* Flag de finalizacion */
   long      ContadorServicios; /* Total de servicios realizados */
   int               ServiciosRunning;  /* Numero de servicios en ejecucion */
   struct timeval  TotalServicio;  /* Tiempo Total de Servicio */
} datos_t;

datos_t *Datos;                         /* Puntero a los datos de memoria global */
long NumIteraciones = 100000;   /* Numero de iteraciones de proceso
- Solo LECTURA */

/* PROGRAMA PRINCIPAL */
int main(int argc, char **argv)
{
   int (*handChild)(); /* Var. para almacenar el gestor de senales de hijos */
   int SocketEscucha;  /* Socket de escucha para el servidor */
   int SocketCliente;  /* Socket de comunicacion con el cliente */
   TDireccion DirServidor;        /* Datos del Servidor */
   TDireccion DirCliente; /* Datos del Cliente */
   int LongCliente;                 /* Longitud de los Datos del Cliente */
   pid_t pid_hijo;                  /* PID del proceso hijo */
   int shmem_id;          /* Identificador para zona de memoria compartida */
   struct timeval ts;     /* Tiempo Medio de Servicio */
   long NumPeticiones =-1;        /* Numero de peticiones a Servir */
   long cPetic = 0;               /* Contador de peticiones a Servir */
   int NumEscuchas = 5;   /* No. de peticiones a escuchar concurrentemente */
   estad_t estad[2];         /* Datos estadisticos de la aplicacion */
   
#  ifdef EXEC_TIME
   struct timeval t1,tp; /* Tiempos de inicio y de programa */   
   /* Obtener el tiempo actual */
   gettimeofday(&t1, NULL);
#  endif

   printf(VERSION);
   /* Comprobar si se solicita ayuda */
   if (argc > 1) {
      if ( strcmp(argv[1],"-?") == 0 || strcmp(argv[1],"-h") == 0 ) {
         printf(SINTAX, argv[0]);
         exit(0);
      }
   }
   /* Obtenemos la lista de argumentos */
   switch(argc) {
      case 4:
         if ( (NumPeticiones = atoi(argv[3])) <= 0 )
            NumPeticiones = -1;
      case 3:
         NumEscuchas = atoi(argv[2]);
         if ( NumEscuchas <= 0 || NumEscuchas >30 )
            NumEscuchas = 5;
      case 2:
         NumIteraciones = atol(argv[1]);
         if ( NumIteraciones <= 0 )
            NumIteraciones = 100000;
      case 1:
         break;
      default:
         printf(SINTAX, argv[0]);
         printf("El numero de argumentos es incorrecto\n");
         exit(0);
         break;
   }

   /* Ignoramos senales de los hijos, pero capturamos su manejador
      para restaurarlo en el caso necesario */
   handChild = (int (*)()) signal(SIGCLD, SIG_IGN);

   /* Crear memoria compartida */
   if ((shmem_id = shmget(SHMEM_KEY, sizeof(datos_t), 0666 | IPC_CREAT)) <0)
      ErrorFatal("Servidor: No se pudo crear la memoria compartida");
   /* Establecer el acceso a la memoria compartida */
   if ((Datos = (datos_t *) shmat(shmem_id, (char *)0, 0)) == (datos_t *) -1)
      ErrorFatal("Servidor: No se pudo enlazar con la memoria compartida");

   /* Crear socket de escucha */
   SocketEscucha = CrearSocketTCP(PUERTO_TCP, &DirServidor);
   /* Enlazar el socket con el puerto de escucha */
   if (bind(SocketEscucha, (struct sockaddr *)&DirServidor,
 sizeof(DirServidor)) < 0)
      ErrorFatal("Servidor: No puedo enlazar Socket con puerto de escucha");
   /* Preparar escucha */
   listen(SocketEscucha, NumEscuchas);

   /* Inicializacion de los datos globales */
   mutex_init(&Datos->lock, MUTEX1_KEY);
   cond_init(&Datos->no_run, COND1_KEY);
   Datos->ContadorServicios = 0;
   Datos->ServiciosRunning = 0;
   Datos->Finalizar = 0;
   Datos->TotalServicio.tv_sec    = 0;
   Datos->TotalServicio.tv_usec = 0;

   /* Obtener los tiempos iniciales */
   getestad(&estad[0]);

   /* Bucle infinito de recepcion de peticiones */
   while (1) {
      /* Comprobar si el servidor ha llegado a la finalizacion */
      if (NumPeticiones >0) {
         if (cPetic == NumPeticiones)
            break;
         cPetic++;
      }
      /* Aceptar peticiones */
      LongCliente = sizeof(DirCliente);
      SocketCliente = accept(SocketEscucha, (struct sockaddr *)&DirCliente,
 &LongCliente);
      if (SocketCliente < 0)
         ErrorFatal("Servidor: Error en la aceptacion de una peticion");
      mutex_lock(&Datos->lock);
      Datos->ServiciosRunning++;
      mutex_unlock(&Datos->lock);

      /* Creamos el Proceso hijo para atender la solicitud */
      switch( (long)(pid_hijo = fork())) {
         /* No se pudo crar hijo */
         case -1: Error("Servidor: No se pudo crear hijo para atender la
 peticion");
                  mutex_lock(&Datos->lock);
                  Datos->ServiciosRunning--;
                  mutex_unlock(&Datos->lock);
                  break;
         /* Proceso HIJO */
         case 0:  close(SocketEscucha);
                  ProcesarPeticion( (void *)SocketCliente );
                  exit(0);
                  break;
         /* Proceso PADRE */
         default: close(SocketCliente);
                  break;
      }
      mutex_lock(&Datos->lock);
      if (Datos->Finalizar) {
         mutex_unlock(&Datos->lock);
         break;
      }
      mutex_unlock(&Datos->lock);
   } /* End-while */

   mutex_lock(&Datos->lock);
   while (Datos->ServiciosRunning >0) {
#     ifdef DEBUG
      printf("Servidor: Finalizando. Espero en la condicion ....\n");
#     endif     
      cond_wait(&Datos->no_run, &Datos->lock);
   }
#  ifdef DEBUG
   printf("Servidor: Finalizando. Salgo de la condicion, voy a cerrar mutex
 y fin\n");
#  endif
   mutex_unlock(&Datos->lock);

   /* Obtener tiempos finales */
   getestad(&estad[1]);       

   /* Destruir los recursos */
   close(SocketEscucha);
   mutex_destroy(&Datos->lock);
   cond_destroy(&Datos->no_run);

   /* Imprime los tiempos de servicio Total y Medio */
   DivTimeInt(&Datos->TotalServicio, Datos->ContadorServicios, &ts);
   printf("\n DATOS ESTADISTICOS DEL SERVIDOR\n");
   printf(  "---------------------------------\n");
   printf("Servidor: Total de peticiones servidas = %d\n",
                Datos->ContadorServicios);
   printf("Servidor: Tiempo Total de servicio = %d sg %d mms\n",
                Datos->TotalServicio.tv_sec, Datos->TotalServicio.tv_usec);
   printf("Servidor: Tiempo Medio de servicio = %d sg %d mms\n",
                ts.tv_sec, ts.tv_usec);

   /* Destruir memoria compartida */
   if ( shmdt((char *)Datos) <0 )
      ErrorFatal("Servidor: No se pudo desenlazar la memoria compartida");
   if ( shmctl(shmem_id, IPC_RMID, (struct shmid_ds *)0) <0 )
      ErrorFatal("Servidor: No se pudo liberar la memoria compartida");

#  ifdef EXEC_TIME
   /* Obtener el tiempo actual y el tiempo de servicio */
   gettimeofday(&tp, NULL);
   DiffTime(&t1, &tp, &tp);
   printf("Servidor: Tiempo de ejecucion = %d sg %d mms\n",
                  tp.tv_sec, tp.tv_usec);
#  endif

   /* Imprimir los tiempos */
   printestad(stderr, &estad[0], &estad[1]);

   /* Finaliza el programa */
   return(0);
}


/*
Proposito: Servir una peticion al servidor mediante un hilo.
Entrada:   arg = Identificador del Socket del Cliente.
Salida:    Ninguna
*/
void *ProcesarPeticion(void *arg)
{
    int Socket = (int)arg;         /* Socket de comunicacion con cliente */
    int LongTrama;                 /* Longitud de la trama recibida */
    char Texto[LONG_TEXTO+1];      /* Texto de la trama recibida */
    long c;                                /* Var.temporal contador */
    struct timeval t1,t2;          /* Tiempos Inicial y Final del servicio  */
    struct timeval ts;              /* Tiempo de servicio */
    int Finalizar = 0;              /* Flag de finalizacion */

    /* Obtener el tiempo actual */
    gettimeofday(&t1, NULL);
#   ifdef DEBUG
    printf("Servicio: Hijo [%d]: En socket %d\n", getpid(), Socket);
#   endif
    /* Recibir Datos */
    LongTrama = RecibirTrama(Socket, Texto);
#   ifdef DEBUG    
    printf("Servicio: Hijo [%d]: Texto '%s' de longitud '%d'\n", getpid(),
                Texto, LongTrama);
    printf("Servicio: Hijo [%d]: Simulando proceso...\n", getpid());
#   endif    
    for (c = 1; c<= NumIteraciones; c++);

    /* Cerrar el servicio */
    close(Socket);

    /* Obtener el tiempo actual y el tiempo de servicio */
    gettimeofday(&t2, NULL);
    DiffTime(&t1, &t2, &ts);

    /* Actualizar el Contador de peticiones servidas */
    mutex_lock(&Datos->lock);
    c = ++(Datos->ContadorServicios);
    AddTime(&Datos->TotalServicio, &ts, &Datos->TotalServicio);
    mutex_unlock(&Datos->lock);
#   ifdef DEBUG
    printf("Servicio: Hijo [%d]: Tiempo de servicio = %d sg %d mms\n",
                getpid(), ts.tv_sec, ts.tv_usec);
    printf("Servicio: Hijo [%d]: Total de peticiones = %d\n",
                getpid(), c);
#   endif
    /* Comprobar si se envio la cadena de finalizacion */
    if (strcmp(Texto, "FIN") == 0)
       Finalizar = 1;

    mutex_lock(&Datos->lock);
    Datos->ServiciosRunning--;
    if (Finalizar)
       Datos->Finalizar = 1;
    if (Datos->ServiciosRunning == 0 && Datos->Finalizar)
       cond_signal(&Datos->no_run);
    mutex_unlock(&Datos->lock);

    exit(0);
    return((void *)0);
}


El servidor y el cliente emplean algunos de los ficheros de la biblioteca de rutinas comunes a todos los ejemplos, para implementar los mútex y variables de condición (a través de semáforos).

De esta forma se obtiene el siguiente Makefile necesario para compilar el ejemplo del cliente/servidor en su versión de procesos :

##############
## Makefile ##
##############
#DEFS=-DDEBUG -DHOST_LOCAL -DEXEC_TIME
DEFS=-DHOST_LOCAL

INSDIR=../../..
INCDIR=$(INSDIR)/include

OBJDIR=$(INSDIR)/libproc
OBJ=$(OBJDIR)/tiempo.o $(OBJDIR)/error.o $(OBJDIR)/getstats.o \
$(OBJDIR)/tcp.o $(OBJDIR)/semaforo.o $(OBJDIR)/mutex.o $(OBJDIR)/cond.o
CC=gcc
CFLAGS=$(DEFS) -I$(INCDIR)
LINK=$(CFLAGS) $(OBJ)
 
#Definicion de dependencias
all : servidor cliente

.c.o:
        $(CC) $(CFLAGS) -c $<

servidor : servidor.o
        $(CC)  $(LINK) servidor.o -o servidor

cliente : cliente.o
        $(CC) $(LINK) cliente.o -o cliente

servidor.o : servidor.c

cliente.o : cliente.c



Ejemplo del Servidor de Sockets con hilos.

Este ejemplo es prácticamente el mismo que el caso anterior, excepto que ahora hemos implementado el servidor mediante hilos en vez de procesos. Para ello se ha utilizado la librería Pthreads del paquete de hilos de Chris Provenzano, aunque es muy posible que el programa funciona igualmente con cualquier otro paquete que siga el estándar POSIX.1c sobre hilos. El programa ha sido ejecutado con dicho paquete instalado bajo el sistema operativo Linux 1.2 y también sobre la versión 2.0.

El cliente, en cambio, no ha sufrido modificación y es el mismo que en la versión de procesos. La apariencia del servidor de la versión con procesos y esta versión multihilo es casi idéntica, de tal forma que cambian muy pocas cosas.

El servidor es el hilo principal, que obtiene los argumentos, crea el socket de comunicación enlazado al puerto de escucha, e inicializa las variables compartidas y el mutex y variable de condición empleados. Cuando se recibe una petición por el puerto de escucha se crea un nuevo hilo descendiente con la primitiva pthread_create() que ejecuta ProcesarPeticion, y se encarga de la gestión de la precisión, mientras que el hilo padre vuelve a bloquearse a la espera de una nueva petición.

El hilo de servicio recibe la trama del cliente y ejecuta un bucle iterativo que simula un tiempo de proceso. Después actualiza las variables globales sobre datos estadísticos de peticiones servidas y tiempo total de servicio. Si el cliente envía una trama con el texto FIN, activa un flag de finalización para el servidor y activa la variable de condición de finalización. Normalmente serán necesarias dos tramas FIN, la primera para activar el flag y la segunda para despertar al hilo servidor padre de la espera por la variable de condición.

El hilo servidor padre comprueba el flag de finalización en cada iteración y cuando está activado, sale del bucle de escucha de peticiones, y se suspende a la espera de la variable de condición. Cuando es despertado por el último hilo, destruye el mútex y la variable de condición, y visualiza una serie de datos estadísticos, como son el número de peticiones servidas, el tiempo total de servicio empleado en servir dichas peticiones y el tiempo medio de servicio por cada petición. La sintaxis del servidor de hilos es idéntica a la versión de procesos.

El listado del servidor de hilos es el siguiente :

SERVIDOR.C :

/****************************************************************************
   EJEMPLO DEL CLIENTE/SERVIDOR.
   Aplicacion:    servidor
   Version:       1.0h
   Fecha: 27/11/1996
   Descripcion: Programa demostrativo del caso del cliente/servidor.
                Se implementa el Servidor como Multihilo. El servidor
                recibe una peticion, y crea un hilo de servicio que gestiona
                dicha peticion.
                El programa emplea las rutinas de la libreria Pthreads, que
                es compatible POSIX.1c
   Autor: Jose Maria Toribio Vicente
   Mail:  jmtoribi@poboxes.com
*****************************************************************************/
#define VERSION "servidor 1.0h - Cliente/Servidor (vers. hilos)\n"
#define SINTAX  "Sintaxis: %s [-?|-h] [num_iter [num_escuchas [num_peticiones]]]\n"

/* #define DEBUG */             /* Define para compilar en modo DEBUG */
/* #define EXEC_TIME */         /* Compilacion con tiempo de ejecucion */
/* #define YIELD */             /* Define para compilar con pthread_yield() */

#include <pthread.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <sys/uio.h>
#include <unistd.h>

#include "tipos.h"
#include "tcp.h"
#include "getstats.h"
#include "tiempo.h"

/* Procedimiento del Servicio */
void *ProcesarPeticion(void *);

/* Estructura de datos empleada para comunicacion entre el servidor de
   escucha y los hilos de servicio */
typedef struct s_datos_t {
   pthread_mutex_t lock;                  /* Mutex de acceso a datos globales */
   pthread_cond_t  no_run;                 /* Condicion de running */
   int               Finalizar;             /* Flag de finalizacion */
   long      ContadorServicios;     /* Total de servicios realizados */
   int               ServiciosRunning;       /* No. de servicios en ejecucion */
   struct timeval  TotalServicio;  /* Tiempo Total de Servicio */
} datos_t;

/* Datos globales */
datos_t Datos = { PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER,
        0, 0, 0 };
long NumIteraciones = 100000; /* No. iteraciones de proceso - Solo LECTURA */


/* PROGRAMA PRINCIPAL */
int main(int argc, char **argv)
{
   int SocketEscucha;      /* Socket de escucha para el servidor */
   int SocketCliente;      /* Socket de comunicacion con el cliente */
   TDireccion DirServidor;        /* Datos del Servidor */
   TDireccion DirCliente; /* Datos del Cliente     */
   int LongCliente;                 /* Longitud de los Datos del Cliente */
   pthread_t id_hilo;     /* PID del proceso hijo */
   int shmem_id;          /* Identificador para zona de memoria compartida */
   struct timeval ts;     /* Tiempo Medio de Servicio */
   long NumPeticiones= -1;        /* Numero de peticiones a Servir */
   long cPetic = 0;               /* Contador de peticiones a Servir */
   pthread_attr_t atributos_hilo; /* Var. de atributos de hilo */
   int NumEscuchas = 5;   /* No. de peticiones a escuchar concurrentemente */
   estad_t estad[2];         /* Datos estadisticos de la aplicacion */

#  ifdef EXEC_TIME   
   struct timeval t1,tp;  /* Tiempos de inicio y de programa */
   /* Obtener el tiempo actual */
   gettimeofday(&t1, NULL);
#  endif

   printf(VERSION);
   /* Comprobar si se solicita ayuda */
   if (argc > 1)
      if ( strcmp(argv[1],"-?") == 0 || strcmp(argv[1],"-h") == 0 ) {
         printf(SINTAX, argv[0]);
         exit(0);
      }
   /* Obtenemos la lista de argumentos */
   switch(argc) {
      case 4:
         if ( (NumPeticiones = atoi(argv[3])) <= 0 )
            NumPeticiones = -1;
      case 3:
         NumEscuchas = atoi(argv[2]);
         if ( NumEscuchas <= 0 || NumEscuchas >30 )
            NumEscuchas = 5;
      case 2:
         NumIteraciones = atol(argv[1]);
         if ( NumIteraciones <= 0 )
            NumIteraciones = 100000;            
      case 1:
         break;
      default:
         printf(SINTAX, argv[0]);
         printf("El numero de argumentos es incorrecto\n");
         exit(0);
         break;
   }

   /* Crear socket de escucha */
   SocketEscucha = CrearSocketTCP(PUERTO_TCP, &DirServidor);
   /* Enlazar el socket con el puerto de escucha */
   if (bind(SocketEscucha, (struct sockaddr *)&DirServidor,
 sizeof(DirServidor)) < 0)
      ErrorFatal("Servidor: No puedo enlazar Socket con puerto de escucha");
   /* Preparar escucha */
   listen(SocketEscucha, NumEscuchas);

   /* Caracteristicas de los hilos de servicio */
   pthread_attr_init(&atributos_hilo);
   pthread_attr_setdetachstate(&atributos_hilo, PTHREAD_CREATE_DETACHED);

   /* Inicializacion de los datos globales */
   pthread_mutex_init(&Datos.lock, NULL);
   pthread_cond_init(&Datos.no_run, NULL);
   Datos.ContadorServicios = 0;
   Datos.ServiciosRunning = 0;
   Datos.Finalizar = 0;
   Datos.TotalServicio.tv_sec  = 0;
   Datos.TotalServicio.tv_usec = 0;

   /* Obtener los tiempos iniciales */
   getestad(&estad[0]);

   /* Bucle infinito de recepcion de peticiones */
   while (1) {
      /* Comprobar si el servidor ha llegado a la finalizacion */
      if (NumPeticiones >0) {
         if (cPetic == NumPeticiones)
            break;
         cPetic++;
      }
      /* Aceptar peticiones */
      LongCliente = sizeof(DirCliente);
      SocketCliente = accept(SocketEscucha, (struct sockaddr *)&DirCliente,
 &LongCliente);
      if (SocketCliente < 0)
         ErrorFatal("Servidor: Error en la aceptacion de una peticion");
      pthread_mutex_lock(&Datos.lock);
      Datos.ServiciosRunning++;
      pthread_mutex_unlock(&Datos.lock);
      if (pthread_create(&id_hilo, &atributos_hilo, ProcesarPeticion,
                (void *)SocketCliente) != 0) {
         Error("Servidor: No se pudo crear hilo para atender la peticion");
         pthread_mutex_lock(&Datos.lock);
         Datos.ServiciosRunning--;
         pthread_mutex_unlock(&Datos.lock);
      }
      pthread_mutex_lock(&Datos.lock);
      if (Datos.Finalizar) {
         pthread_mutex_unlock(&Datos.lock);
         break;
      }
      pthread_mutex_unlock(&Datos.lock);
   } /* End-while */

   pthread_mutex_lock(&Datos.lock);
   while (Datos.ServiciosRunning >0) {
#     ifdef DEBUG
      printf("Servidor: Finalizando. Espero en la condicion ....\n");
#     endif     
      pthread_cond_wait(&Datos.no_run, &Datos.lock);
   } 
#  ifdef DEBUG
   printf("Servidor: Finalizando. Salgo de la condicion, voy a cerrar mutex
 y fin\n");
#  endif
   pthread_mutex_unlock(&Datos.lock);

   /* Obtener tiempos finales */
   getestad(&estad[1]);

   /* Destruir los recursos */
   close(SocketEscucha);
   pthread_mutex_destroy(&Datos.lock);
   pthread_cond_destroy(&Datos.no_run);
   pthread_attr_destroy(&atributos_hilo);

   /* Imprime los tiempos de servicio Total y Medio */
   DivTimeInt(&Datos.TotalServicio, Datos.ContadorServicios, &ts);
   printf("\n DATOS ESTADISTICOS DEL SERVIDOR\n");
   printf(  "---------------------------------\n");
   printf("Servidor: Total de peticiones servidas = %d\n",
                Datos.ContadorServicios);
   printf("Servidor: Tiempo Total de servicio = %d sg %d mms\n",
                Datos.TotalServicio.tv_sec, Datos.TotalServicio.tv_usec);
   printf("Servidor: Tiempo Medio de servicio = %d sg %d mms\n",
                ts.tv_sec, ts.tv_usec);

#  ifdef EXEC_TIME
   /* Obtener el tiempo actual y el tiempo de servicio */
   gettimeofday(&tp, NULL);
   DiffTime(&t1, &tp, &tp);
   printf("Servidor: Tiempo de ejecucion = %d sg %d mms\n",
                  tp.tv_sec, tp.tv_usec);
#  endif

   /* Imprimir los tiempos */
   printestad(stderr, &estad[0], &estad[1]);

   /* Finaliza el programa */
   return(0);
}


/*
Proposito: Servir una peticion al servidor mediante un hilo.
Entrada:   arg = Identificador del Socket del Cliente.
Salida:    Ninguna
*/
void *ProcesarPeticion(void *arg)
{
    int Socket = (int)arg;         /* Socket de comunicacion con cliente */
    int LongTrama;                 /* Longitud de la trama recibida */
    char Texto[LONG_TEXTO+1];      /* Texto de la trama recibida */
    long c;                                /* Var.temporal contador */
    struct timeval t1,t2;          /* Tiempos Inicial y Final del servicio  */
    struct timeval ts;              /* Tiempo de servicio */
    int Finalizar = 0;              /* Flag de finalizacion */

    /* Obtener el tiempo actual */
    gettimeofday(&t1, NULL);
#   ifdef DEBUG
    printf("Servicio: Hilo [%d]: En socket %d\n", pthread_self(), Socket);
#   endif
    /* Recibir Datos */
    LongTrama = RecibirTrama(Socket, Texto);
#   ifdef DEBUG
    printf("Servicio: Hilo [%d]: Texto '%s' de longitud '%d'\n",
 pthread_self(), Texto, LongTrama);
    printf("Servicio: Hilo [%d]: Simulando proceso...\n", pthread_self());
#   endif
    for (c = 1; c<= NumIteraciones; c++);

    /* Cerrar el servicio */
    close(Socket);

    /* Obtener el tiempo actual y el tiempo de servicio */
    gettimeofday(&t2, NULL);
    DiffTime(&t1, &t2, &ts);

    /* Actualizar el Contador de peticiones servidas */
    pthread_mutex_lock(&Datos.lock);
    c = ++(Datos.ContadorServicios);
    AddTime(&Datos.TotalServicio, &ts, &Datos.TotalServicio);
    pthread_mutex_unlock(&Datos.lock);
#   ifdef DEBUG
    printf("Servicio: Hilo [%d]: Tiempo de servicio = %d sg %d mms\n",
                pthread_self(), ts.tv_sec, ts.tv_usec);
    printf("Servicio: Hilo [%d]: Total de peticiones = %d\n",
                pthread_self(), c);
#   endif

    /* Comprobar si se envio la cadena de finalizacion */
    if (strcmp(Texto, "FIN") == 0)
       Finalizar = 1;

    pthread_mutex_lock(&Datos.lock);
    Datos.ServiciosRunning--;
    if (Finalizar)
       Datos.Finalizar = 1;
    if (Datos.ServiciosRunning == 0 && Datos.Finalizar)
       pthread_cond_signal(&Datos.no_run);
    pthread_mutex_unlock(&Datos.lock);

    pthread_exit(NULL);
    return ((void *)0);
}


El servidor emplea los ficheros de cabecera INCLUDE.H, PARAM.H, TIPOS.H, ERROR.H, TCP.H y TIEMPO.H, y los ficheros fuente ERROR.C, TCP.C y TIEMPO.C empleados también en la versión de procesos y que pertenecen a la biblioteca de rutinas comunes a los ejemplos. El código del cliente es el mismo que en la versión de procesos.

El Makefile necesario para compilación es el siguiente :

##############
## Makefile ##
##############
#DEFS=-DDEBUG -DHOST_LOCAL -DEXEC_TIME
DEFS=-DHOST_LOCAL

INSDIR=../../..
INCDIR=$(INSDIR)/include

OBJDIR=$(INSDIR)/libpthread
OBJ=$(OBJDIR)/tiempo.o $(OBJDIR)/error.o $(OBJDIR)/getstats.o $(OBJDIR)/tcp.o
CC=pgcc
CFLAGS=$(DEFS) -I$(INCDIR)
LINK=$(CFLAGS) $(OBJ)
 
#Definicion de dependencias
all :   servidor cliente

.c.o:
        $(CC) $(CFLAGS) -c $<

servidor : servidor.o
        $(CC)  $(LINK) servidor.o -o servidor

cliente : cliente.o
        $(CC) $(LINK) cliente.o -o cliente

servidor.o : servidor.c

cliente.o : cliente.c



Comparación de las dos implementaciones.

Ambas implementaciones, como se podrá haber observado, son muy similares no solo conceptualmente sino a nivel de programación, ya que se dispone de las rutinas de mútex y variables de condición (en la versión de procesos) que enmascaran las llamadas a rutinas de semáforos, al igual que sucedía en el ejemplo del productor/consumidor.

Como referencia para discutir las ventajas e inconvenientes de las dos implementaciones : multihilo y procesos, se indican los siguientes tiempos de ejecución obtenidos como muestra en un sistema formado por una CPU Intel 486-66Mhz - 9Mb RAM - Linux 1.2.3, y con carga mínima en el sistema. En el sistema se ejecutaba el procesos servidor en un terminal virtual y un proceso que realiza 30 llamadas al cliente con una trama de prueba, en un segundo terminal virtual, además de las dos tramas FIN para indicar la finalización al servidor, repitiéndose la prueba en siete ocasiones para las dos implementaciones.

El ejemplo que se presenta a continuación se realizó con 500000 iteraciones de simulación. Los tiempos están en segundos y milésimas de segundo.

MUESTRAS
1
2
3
4
Media
Hilos. Pet. Serv.
32
32
32
32
32
Hilos. T. Total Serv.
66,724
78,104
83,284
75,447
75,89
Hilos. T.Medio Serv.
2,647
2,437
2,602
2,357
2,511
Proc. Pet. Serv.
32
32
32
32
32
Proc. T. Total Serv.
504,764
652,579
657,610
588,263
600,804
Proc. T.Medio Serv.
15,773
20,393
20,550
18,383
18,775

Figura 7-2 Tiempos de las dos implementaciones.

Inconvenientes de la versión de procesos :

Ventajas de la versión de procesos :

Inconvenientes de la versión multihilo :

Como inconvenientes de la versión multihilo se pueden destacar los siguientes :

Ventajas de la versión multihilo :