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 Napster, KaZaA, y Gnutella. En esta sección veremos las bases de comunicación de estos sistemas.


Clasificación de Servidores

    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.

É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 traslapar 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

  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). 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. El par (LocalPort, LocalIP, remotePort, RemoteIP) define cada conexión.
  2. Llamados para obtener información sobre la conexión: gethostname(), gethostbyname(), inet_ntoa(), inet_addr(), getsockname(), getpeername(), setsockopt().
  3. Configuración de puerta como reusable. Mostrar Diagrama de estado de la conexión TCP. Si el servidor muere primero, el puerto queda tomado por un tiempo.
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 (Se deben eliminar carreras críticas cuando se requiere comunicación entre los servidores de cada cliente),
  2. El uso de hilos es similar al caso anterior a cambio de procesos,
  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.


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:


Ejemplos

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