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 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:
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);
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.