Sockets
El viernes pasado, mi "jefe" (ojala todos los jefes fuesen asi) nos planteaba a mi y al otro profesor de practicas una duda que le habia surgido a raiz de la practica 1 de Sistemas operativos de este año. La duda era si una vez te conectas a traves de socket a un servidor habria que hacer alguna gestion de puertos o si no habria ningun problema en ello. Pero primero habria que explicar un poco de que va la practica.
La parte practica de la asignatura se divide en 3 practicas: Inferno, Purgatorio y Paradiso. En la primera de ellas, que es en la que se trabaja con sockets, hay que diseñar una arquitectura cliente-servidor en la que n clientes (10 por limitaciones a los usuarios para que nadie pueda crear un numero indefinido de hijos y saturar el sistema) se conectan a un servidor que les permite interactuar con otros clientes. Disponemos de un puerto predefinido que el servidor "abrira" y utilizara para "escuchar" peticiones de conexion.
La conexion se produce a traves de un socket. Para quien no sepa que es un socket, esto es un vinculo entre una IP + puerto y otra IP + puerto a traves de una red, y utilizando un protocolo determinado.
La duda era: Si solo utilizamos un puerto en el servidor, que sucedera cuando se conecten los diferentes clientes? No se producira una colision? Al crear el socket, no se establecera la comunicacion y sera imposible que mas clientes puedan comunicarse utilizando el mismo puerto en el servidor? Es decir, si el servidor tiene la IP x.x.x.x y abrimos el puerto 666, y el cliente tiene la IP y.y.y.y e intenta abrir una conexion, el socket no sera (x.x.x.x + 666 , y.y.y.y + puerto_cliente) ?
Esta es una duda tipica que surge al diseñar y programar esta practica.
Evidentemente no es esto lo que pasa, ya que si asi fuese seria imposible hacer que un servidor aceptase mas de una peticion y solo podriamos diseñar modelos de igual a igual (p2p). Entonces, que sucede realmente en el servidor? Miremos el siguiente codigo:
Bien, es un fragmento de codigo de un servidor tipico. Las lineas interesantes y a comentar son la del bind, la del listen y la del accept, que son las que contestan a la pregunta.
La linea del bind lo que hace es crear un file descriptor que simbolizara el puerto por el que "escuchamos" la red. Si la funcion tiene exito, podremos hacer un write por ese file descriptor y lo que escribamos se transmitira.
Ya tenemos nuestra conexion creada y simbolizada en un file descriptor. Ahora utilizamos la funcion listen para hacer que el programa escuche el puerto a la espera de alguna comunicacion. Esto es, nuestro programa le cedera el control al sistema y este tan solo volvera al programa cuando reciba comunicacion a traves del puerto designado.
Y aqui es donde llega la madre del cordero. Cuando la funcion listen devuelve el control es que alguien intenta conectar. Pero aun no esta conectado tal y como entendemos eso de manera intuitiva, sino que el sistema lo tiene en una cola de conexiones pendientes. Ahora, con la llamada a accept, lo que hacemos es sacar la primera peticion de la cola y crear un socket, pero no lo creamos con el puerto de escucha sino que el sistema nos asigna uno de los puertos libres para crear la conexion.
Espero que con esto haya quedado mas claro lo que sucede con todo el tema de los sockets, que hasta que no se trabaja un poco con ello es bastante abstracto.
La parte practica de la asignatura se divide en 3 practicas: Inferno, Purgatorio y Paradiso. En la primera de ellas, que es en la que se trabaja con sockets, hay que diseñar una arquitectura cliente-servidor en la que n clientes (10 por limitaciones a los usuarios para que nadie pueda crear un numero indefinido de hijos y saturar el sistema) se conectan a un servidor que les permite interactuar con otros clientes. Disponemos de un puerto predefinido que el servidor "abrira" y utilizara para "escuchar" peticiones de conexion.
La conexion se produce a traves de un socket. Para quien no sepa que es un socket, esto es un vinculo entre una IP + puerto y otra IP + puerto a traves de una red, y utilizando un protocolo determinado.
La duda era: Si solo utilizamos un puerto en el servidor, que sucedera cuando se conecten los diferentes clientes? No se producira una colision? Al crear el socket, no se establecera la comunicacion y sera imposible que mas clientes puedan comunicarse utilizando el mismo puerto en el servidor? Es decir, si el servidor tiene la IP x.x.x.x y abrimos el puerto 666, y el cliente tiene la IP y.y.y.y e intenta abrir una conexion, el socket no sera (x.x.x.x + 666 , y.y.y.y + puerto_cliente) ?
Esta es una duda tipica que surge al diseñar y programar esta practica.
Evidentemente no es esto lo que pasa, ya que si asi fuese seria imposible hacer que un servidor aceptase mas de una peticion y solo podriamos diseñar modelos de igual a igual (p2p). Entonces, que sucede realmente en el servidor? Miremos el siguiente codigo:
...
// Asociamos nuestra direccion y puerto a un file descriptor.
// La estructura servidor esta previamente inicializada.
error=bind(sock, (struct sockaddr*) &servidor, sizeof(servidor));
if (error<0) {
printf("Error en el \"binding\"!\n");
exit(error);
}
write(1,"SERVER OK - Esperando comunicacion...\n\n",37);
while (1) {
// Escuchamos por el file descriptor, o lo que es lo
// mismo, escuchamos por el puerto indicado.
listen(sock,1);
// Inicialitzamos el tamaño del cliente (no tiene nada que
// ver con el problema).
clilen=sizeof(client);
// Aceptamos la comunicacion entrante y guardamos el file
// descriptor en newsock.
newsock=accept(sock,(struct sockaddr *) &client, &clilen);
// Forknicamos (no explicare aqui la estructura cliente-servidor)
pid=fork();
if (pid==0) {
close(sock);
// Controlamos el error
if (newsock<0) {
printf("Error en el nuevo socket\n");
exit (0);
}
// Y si no hay error...
else {
printf("Puerto donde recibimos el
accept: %d\n\n",client.sin_port);
while (1) {
...
Bien, es un fragmento de codigo de un servidor tipico. Las lineas interesantes y a comentar son la del bind, la del listen y la del accept, que son las que contestan a la pregunta.
La linea del bind lo que hace es crear un file descriptor que simbolizara el puerto por el que "escuchamos" la red. Si la funcion tiene exito, podremos hacer un write por ese file descriptor y lo que escribamos se transmitira.
Ya tenemos nuestra conexion creada y simbolizada en un file descriptor. Ahora utilizamos la funcion listen para hacer que el programa escuche el puerto a la espera de alguna comunicacion. Esto es, nuestro programa le cedera el control al sistema y este tan solo volvera al programa cuando reciba comunicacion a traves del puerto designado.
Y aqui es donde llega la madre del cordero. Cuando la funcion listen devuelve el control es que alguien intenta conectar. Pero aun no esta conectado tal y como entendemos eso de manera intuitiva, sino que el sistema lo tiene en una cola de conexiones pendientes. Ahora, con la llamada a accept, lo que hacemos es sacar la primera peticion de la cola y crear un socket, pero no lo creamos con el puerto de escucha sino que el sistema nos asigna uno de los puertos libres para crear la conexion.
Espero que con esto haya quedado mas claro lo que sucede con todo el tema de los sockets, que hasta que no se trabaja un poco con ello es bastante abstracto.
