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
- Espera por la llegada de un cliente
- Procesa el requerimiento de un cliente
- Envía la respuesta a ese cliente
- 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. Este tipo de servidor atiende 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í:
- Espera por la llegada de un cliente
- 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.
- 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:
- Valores de puertas asignadas cuando se
establece
una conexión. La 4-tupla (LocalPort, LocalIP, remotePort, RemoteIP)
define cada conexión TCP.
- 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
correspondiente buscar por "reentrant"
- Configuración de puerto como reusable. El cliente o servidor que termina primero deja tomado el puerto por un tiempo. Si el servidor termina primero,
no podremos usar el mismo puerto por un rato (~90 [s]).
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:
- Espera por la llegada de un cliente o nuevo
requerimiento de un cliente ya conectado.
- Acepta la conexión de un nuevo cliente o
procesa requerimiento de uno ya conectado
- Si se atendió un requerimiento,
envía la respuesta al cliente que envió el requerimiento
- 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:
- 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.
- 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
- 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".
- 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.
En resumen, veremos el select dado que es soportado
más ampliamente, y es simple pues en todo momento 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
- 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.
- tv_sec = tv_usec = 0: No
espera. Todos los descriptores son chequeados y se retorna
inmediatamente.
- 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:
- -1: Ocurrió un error. Por ejemplo si
llegó una señal.
- 0: no hay actividad en ninguno de los
descriptores.
- n: número de descriptores que
presentan actividad.
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 especificado 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
- Servidor
Iterativo Cliente
- Leyendo entradas de múltiples terminales
(o ventanas xterm o telnet) select