Modelo Cliente-Servidor y Multiplexión de I/O (select)

El modelo cliente-servidor (similar al del teléfono) se usa en muchas aplicaciones. Otro modelo muy común en la actualidad es el peer-to-peer, el cual se ve por ejemplo en aplicaciones tipo Messanger y Skype. En esta sección veremos las bases de comunicación de estos sistemas.


Clasificación de Servidores
: Por la forma como un servidor atiende las peticiones entrantes, lo podemos clasificar como Iterativo o Concurrente.
    Servidor Iterativo
  1. Espera por la llegada de un cliente
  2. Procesa el requerimiento de un cliente
  3. Envía la respuesta al cliente que envió el requerimiento
  4. Regresa al primer paso
Como consecuencia de esta interacción sólo un cliente puede ser atendido a la vez. Si la generación de la respuesta a un cliente demora,  otros requerimientos no pueden ser atendidos concurrentemente. Los servidores aquí atienden un requerimiento por vez.

Éste es el caso típico de servidores UDP (UDPServer.c  UDPClient.c). Como no hay una conexión establecida, los requerimientos de varios clientes se pueden intercalar conservando "atómicamente" las secuencias requerimiento y respuesta para cada cliente.

Un servidor Iterativo basado en TCP es menos conveniente. En este caso hay una conexión con el cliente y sólo podemos atender a otro cliente cuando el previo ha concluido todas sus interacciones requerimiento-respuesta. La postergación de los otros clientes es mayor comparada con servidor UDP.

    Servidor Concurrente: trabaja así:

  1. Espera por la llegada de un cliente
  2. Inicia un servidor para manejar los requerimientos del cliente. Esto involucra la creación de un nuevo proceso o hilo. Cuando el cliente se va (termina) el proceso o hilo también termina.
  3. Regresa al primer paso.
Éste es el caso típico de servidores TCP (TCPServer.c  TCPClient.c) con procesos o hebras a cargo de cada cliente. El paso tres es alcanzado tan pronto como se inicia la atención del Cliente, por lo tanto múltiples clientes pueden ser atendidos en forma concurrente. 

Destacar:

  1. Valores de puertas asignadas cuando se establece una conexión. La 4-tupla (LocalPort, LocalIP, remotePort, RemoteIP) define cada conexión TCP.
  2. Llamados para obtener información sobre la conexión: gethostname(), gethostbyname(), inet_ntoa(), inet_addr(), getsockname(), getpeername(), setsockopt(). Notar que algunas de estas funciones no garantizan re-entrancia. En páginas man buscar por "reentrant"
  3. Configuración de puerta como reusable. Mostrar Diagrama de estado de la conexión TCP. El que termina primero deja tomado el puerto por un tiempo. Si el servidor termina primero, no podremos usar el mismo puerto por un rato.
Una situación intermedia es creada cuando un servidor TCP atiende múltiples clientes en forma secuencial por requerimiento y no por conexión. Es un caso parecido al servidor Iterativo UDP, pero implementado con TCP. En este caso tenemos:
  1. Espera por la llegada de un cliente o nuevo requerimiento de un cliente ya conectado.
  2. Acepta la conexión de un nuevo cliente o procesa requerimiento de uno ya conectado
  3. Si se atendió un requerimiento, envía la respuesta al cliente que envió el requerimiento
  4. Regresa al primer paso
Para poder esperar requerimientos que pueden llegar por múltiples descriptores, se dispone de la función select().
Algunas otras técnicas que se parecen a select, pero no siempre son las más adecuadas, son:
  1. Creación de múltiples procesos y cada uno atiende cada descriptor. Esta opción genera complicaciones cuando hay datos  compartidos entre las conexiones. Por ejemplo en un chat en que participan varios. Lo que uno recibe se debe enviar al resto, pero los otros están en otros procesos.
  2. El uso de hilos es similar al caso anterior. Aquí los datos se pueden compartir, pero se deben eliminar carreras críticas o inconsitencias cuando se requiere comunicación entre los servidores de cada cliente
  3. Uso de I/O sin bloqueo (nonbloking I/O). En este caso el servidor hace una encuesta descriptor por descriptor identificando si hay algún requerimiento: Si no lo hay, se retorna inmediatamente y se consulta el otro descriptor. Conduce a sistemas con "Busy-wait".
  4. Uso de I/O asíncrono. Se anuncia deseo de hacer I/O y el proceso es interrumpido cuando tiene algo. Esta opción no es soportada por todos los sistemas operativos.
  5. En resumen, veremos el select que es soportado más ampliamente, y es simple pues en todo moento tenemos un sólo hilo (secuencia en ejecución)


Las funciones select y poll proveen un mecanismo para que los programas chequeen un grupo de descriptores y sepan cuando existe entradas, salidas, o alguna condición de excepción en alguno de ellos.

#include <sys/types.h>
#include <sys/time.h>

int select ( int maxfd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval * timeout_ptr );

Macros para manipular el conjunto de descriptores:
void FD_SET ( int fd, fd_set * fdset);
  /* agrega un descriptor del conjunto de descriptores */
void FD_CLR ( int fd, fd_set * fdset); /* remueve un sescriptor del conjunto */
int FD_ISSET ( int fd, fd_set * fdset);  /* retorna verdadero si el descriptor es parte del conjunto */
void FD_ZERO ( fd_set * fdset);  /* desactiva todos los descriptores del conjunto pasados como argumento
equivale a remover los elementos del conjunto */

struct timeval {
    long tv_sec;    /* segundos*/
    long tv_usec;    /* micro segundos */
}
 

Valores para timeout_ptr
  1. NULL : Espera por siempre. Espera infinita por actividad en alguno de los descriptores. Esta espera puede ser interrumpida por la llegada de una señal.
  2. tv_sec = tv_usec = 0:    No espera. Todos los descriptores son chequeados y se retorna inmediatamente.
  3. tv_sec != 0 ó tv_usec != 0 : Se espera hasta el tiempo indicado. Si hay actividad antes, se retorna inmediatamente, sino se retorna como el caso anterior al término del timeout.
Valores retornado por select: La función pselect es una variante de select.
int   pselect(int   n,   fd_set   *readfds,  fd_set  *writefds,  fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);
La principal diferencia con select es el tipo de dato para especificar el tiempo de espera. Ahora el tiempo es espcificado en segundos y nanosegundos. Si el último argumento es null, el comportamiento es como en select. Si es no nulo, este valor define máscara de señales que serán aceptadas mientras se espera.

Ejemplos

  1.  Servidor Iterativo Cliente
  2. Leyendo entradas de múltiples terminales (o ventanas xterm o telnet) select