Ejemplos java y C/linux

Tutoriales

Enlaces

Licencia

Creative Commons License
Esta obra está bajo una licencia de Creative Commons.
Para reconocer la autoría debes poner el enlace http://www.chuidiang.com

Atender a varios clientes.
Sockets con select()

Normalmente a un programa servidor se le pueden conectar varios clientes simultáneamente. A un servidor de páginas web se le pueden conectar a la vez varios navegadores, al servidor del juego Quake se le pueden conectar a la vez varios jugadores, etc. Por ello un programa servidor debe estar preparado para esta circunstancia.

Hay dos opciones posibles.

La primera opción, la de múltiples procesos/hilos, es adecuada cuando las peticiones de los clientes son muy numerosas y nuestro servidor no es lo bastante rápido para atenderlas consecutivamente. Si, por ejemplo, los clientes nos hacen en promedio una petición por segundo y tardamos cinco segundos en atender cada petición, es mejor opción la de un proceso por cliente.  Así, por lo menos, sólo sentirá el retraso el cliente que más pida. (En un ejemplo tan exagerado como este, posiblemente nos de igual lo que hagamos, que no seremos capaz de atender a los clientes).

La segunda es buena opción cuando recibimos peticiones de los clientes que podemos atender más rápidamente de lo que nos llegan. Si los clientes nos hacen una petición por segundo y tardamos un milisegundo en atenderla, nos bastará con un único proceso pendiente de todos.

Vamos a hacer un ejemplo de código con esta segunda opción. Un programa servidor atenderá conexiones de clientes. A cada uno le asignará un número de cliente y se lo enviará. Tendremos un programa cliente que cada segundo envía su número de cliente al servidor. Podremos lanzar hasta 10 clientes simultaneamente y todos serán atendidos. En el ejemplo básico de sockets se hicieron unos ficheros con funciones útiles. Aquí se han extraido en una librería que se necesita para el ejemplo.

Para facilitarnos el tratamiento con un solo proceso, tenemos la función select(), Si tenemos varios sockets abiertos (incluido el socket servidor que recibe a los clientes) y disponemos de sus descriptores, podemos pasarselos a la función select(). Si así lo deseamos, nuestro código se quedará dormido hasta que en alguno de los descriptores haya datos disponibles (un nuevo cliente que entra o un cliente ya existente que nos envía un mensaje).

Los parámetros de la función select() son los siguientes:

Cuando la función retorna, nos cambia los contenidos de los fd_set para indicarnos qué descriptores de fichero tiene algo. Por ello es importante inicializarlos completamente antes de volver a llamar a la función select().

Estos fd_set son unos punteros un poco raros. Para rellenarlos y ver su contenido tenemos una serie de macros:


En nuestro programa de ejemplo del servidor tendremos un descriptor del socket servidor y un array con 10 descriptores para clientes. Inicializaremos fd_set con un FD_ZERO(), luego le añadiremos el socket servidor y finalmente, con un bucle, los sockets clientes. Después llamaremos a la función select(). El código sería más o menos

fd_set descriptoresLectura;
int socketServidor;
int socketCliente[10];
int numeroClientes;
...
FD_ZERO (&descriptoresLectura);
FD_SET (socketServidor, &descriptoresLectura);
for (i=0; i<numeroClientes; i++)
    FD_SET (socketCliente[i], &descriptoresLectura);
...
select (maximo+1, &descriptoresLectura, NULL, NULL, NULL);

Como no tenemos interes en condiciones de escritura ni excepciones, pasamos NULL en el segundo y tercer parámetro. El último lo ponemos también a NULL puesto que no tenemos otra tarea que hacer hasta que alguien se conecte o nos envíe algo.

Cuando se salga del select() es porque: 1) se ha intentado conectar un nuevo cliente, 2) uno de los clientes ya conectados nos ha enviado un mensaje o bien 3) uno de los clientes ya conectados ha cerrado la conexión. En cualquiera de estas circunstancias, tenemos que hacer el tratamiento adecuado. La función select() sólo nos avisa de que algo ha pasado, pero no acepta automáticamente al nuevo cliente, no lee su mensaje ni cierra su socket.

Por ello, detrás del select(), debemos verificar socketServidor para ver si hay un nuevo cliente y todos los socketCliente[], para ver si nos han enviado algo o cerrado el socket. El código, después del select(), sería:

/* Se tratan los clientes */
for (i=0; i<numeroClientes; i++)
{
    if (FD_ISSET (socketCliente[i], &descriptoresLectura))
    {
        if ((Lee_Socket (socketCliente[i], (char *)&buffer, sizeof(int)) > 0))
        {
            /* Se ha leido un dato del cliente correctamente. Hacer aquí el tratamiento para ese mensaje. En el ejemplo, se lee y se escribe en pantalla. */
        }
        else
        {
            /* Hay un error en la lectura. Posiblemente el cliente ha cerrado la conexión. Hacer aquí el tratamiento. En el ejemplo, se cierra el socket y se elimina del array de socketCliente[] */
        }
    }
}

/* Se trata el socket servidor */
if (FD_ISSET (socketServidor, &descriptoresLectura))
{
    /* Un nuevo cliente solicita conexión. Aceptarla aquí. En el ejemplo, se acepta la conexión, se mete el descriptor en socketCliente[] y se envía al cliente su posición en el array como número de cliente. */
}

La función Lee_Socket() forma parte de la librería que se comentó anteriormente. Devuelve lo mismo que la función read(), es decir, el número de bytes leidos, 0 si se ha cerrado el socket o -1 si ha habido error.

En cuanto al código de ejemplo del cliente, poco tiene que decir. Abre la conexión, recibe un número de cliente del servidor y se lo reenvia una vez por segundo.

En primer lugar, pera ejecutar el ejemplo, necesitas una mini librería de socket que he hecho para no tener que repetir el mismo código en todos los ejemplos.

Una vez que tengas la librería, tienes los códigos de ejemplo en servselect.c y clientselect.c, que se compilan con Makefile. Descárgalos en un directorio distinto al de la librería (ya que el fichero Makefile, aunque con el mismo nombre, es distinto del de la librería), quita la extensión .txt. Edita el Makefile del ejemplo y en la línea que pone

LIBCHSOCKET = ../LIBRERIA

cambia ../LIBRERIA por el path donde hayas descargado y compilado la librería. Compila el ejemplo con el comando make.

Con permisos de root, en el fichero /etc/services debes añadir una línea que ponga

cpp_java    15557/tcp

El número puede ser el que tú quieras entre 1024 y 65535 siempre y cuando no exista ya en el fichero. El nombre cpp_java aparece tal cual en el código del ejemplo. Si quieres puedes poner otro nombre en el /etc/services, pero debes cambiarlo también en los fuentes del ejemplo.

Una vez compilado todo, ejecuta el servidor con ./servselect. Luego puedes ejecutar en varias ventanas tantos clientes ./clientselect como desees. Verás como todos son atendidos, hasta un máximo de 10 simultáneamente.

Estadísticas y comentarios

Numero de visitas desde el 4 Feb 2007: