Diseño de servicios con y sin estado: Servicio RDIR

Se trata de una práctica de carácter individual, con un peso de 2 sobre 10 en la nota final de la asignatura. El plazo de entrega de la práctica tanto en la convocatoria de junio como en la de septiembre coincide con el día del examen de la asignatura.


NOTA IMPORTANTE: Aunque se puede desarrollar la práctica en cualquier máquina Linux, ésta debe funcionar en triqui, que es donde se lleva a cabo la corrección de la misma. Se debe tener en cuenta que triqui es una máquina de 64 bits, lo que puede afectar a operaciones que mezclan tipos de datos (por ejemplo, la asignación de un tipo puntero a un int no funcionará correctamente en este tipo de arquitecturas).

Objetivo de la práctica

El objetivo principal es que el alumno pueda ver de una forma aplicada la diferencia que existe, a la hora de diseñar un servidor, entre usar un modelo de servicio con estado frente a utilizar uno sin estado. Por tanto, como fruto del desarrollo de este ejercicio, el alumno debe poder llegar a constatar de forma práctica las principales ventajas de cada modelo de servicio alternativo, que, como ya se estudió en la parte teórica de la asignatura, son las siguientes:

Asimismo, dado que en las distintas versiones de la práctica, la comunicación se realizará mediante distintos mecanismos de comunicación, sockets y RPC, el alumno podrá percibir qué diferencias existen cuando se trabaja en distintos niveles de abstracción.

Descripción general del servicio

Se pretende ofrecer a las aplicaciones un conjunto de servicios que permitan acceder a un directorio remoto. Estas funciones van a ser similares a las funciones estándar POSIX para acceso a los directorios (opendir, readdir y closedir). Los prototipos de las funciones que hay que implementar son los siguientes (fichero dirops.h):
typedef void RDIR;

RDIR *r_opendir(char *dirname);

char *r_readdir(RDIR *dir);

int r_closedir(RDIR *dir);

Como se puede observar, los prototipos son como los definidos en POSIX, excepto en 2 aspectos:

Por lo demás, estas funciones tienen el mismo comportamiento que las de POSIX.

Arquitectura del servicio

La práctica consiste en programar tres versiones del servicio de acceso remoto a un directorio que se acaba de describir:

Con independencia de qué versión se trate, el servicio distribuido estará organizado en los siguientes componentes:

Aspectos generales sobre las comunicaciones usadas en la práctica

En las tres versiones de la práctica se usará una comunicación de tipo TCP por su mayor fiabilidad. En el caso de las versiones basadas en sockets, se tratará, por tanto, de sockets stream, mientras que en el caso de la versión basada en RPC, se usará RPC sobre TCP.

La conexión TCP presenta un problema en lo que se refiere a su uso para esta práctica. Se trata de un tipo de comunicación inherentemente con estado. Por tanto, aunque se desarrolle en el nivel de aplicación un servicio sin estado (como ocurre con la segunda y tercera versión de la práctica), que teóricamente permite que el cliente pueda seguir obteniendo servicio de un servidor después de que éste se caiga y vuelva a arrancar, el nivel de comunicaciones TCP no lo permitirá ya que se habrá perdido la conexión con el servidor. Ante esta situación, se plantean dos soluciones de cara a la práctica:

Con respecto al posible carácter heterogéneo de las máquinas, recuérdese que el mecanismo de RPC lo maneja de forma transparente, pero no es así en el caso de los sockets. En la versiones basadas en sockets se deben usar las funciones de transformación (htonl, ntohl, htons y ntohs) para solventar estos problemas.

Otro aspecto importante que afecta a las tres versiones de la práctica es el del direccionamiento del servidor: de qué manera el cliente puede localizar al servidor. A continuación, se analiza este aspecto para los dos tipos de comunicación utilizados.

Direccionamiento con sockets

Por simplicidad, se utilizará un enlace estático entre los clientes y el servidor, no existiendo, por lo tanto, un proceso que juegue el papel de binder. El servidor recibirá como argumento el número de puerto por el que recibirá las peticiones. En cuanto al cliente, la biblioteca de servicio (fichero dirops.c) debe encargarse de obtener de las variables de entorno SERVIDOR y PUERTO el nombre de la máquina donde ejecuta el servidor y el puerto por el que está escuchando, respectivamente.

Para evitar colisiones en el uso de los puertos, cada alumno deberá utilizar para sus pruebas números de puerto cuyos cuatro últimos dígitos coincidan con los cuatro últimos del número de su matrícula (así, por ejemplo, si el número de matrícula fuera 90123, se deberían usar números de puerto tales como 10123, 20123, 30123, ...). Asimismo, se debe usar la opción SO_REUSEADDR para forzar la reutilización de los puertos.

Direccionamiento con RPCs

Dado que se trata de un servicio basado en las RPC de Sun, es necesario asignar un número de programa a este servicio. Siguiendo las recomendaciones de Sun, que plantean que los programas desarrollados por los usuarios tengan números mayores que 2 elevado a la 29, y para evitar las colisiones entre alumnos que desarrollen la práctica en el mismo sistema, se recomienda usar la siguiente estrategia a la hora de asignar el número de programa:

Para facilitar la corrección de las prácticas, el alumno no debe especificar el número de programa elegido dentro de la sentencia program del fichero de IDL (rdir.x), sino en la siguiente línea de dicho fichero:

%#define NUMPROG 667654321

Por otro lado, para que el cliente pueda conocer en qué máquina está ejecutando el servidor, antes de ejecutar el cliente, se deberá definir la variable de entorno SERVIDOR con el nombre de la máquina donde ejecuta el servidor. La biblioteca de servicio (fichero dirops.c) debe encargarse de leer esta variable.

Desarrollo del servicio con estado basado en sockets

El desarrollo correcto de este servicio proporcionará una nota de 3 puntos.

En primer lugar, hay que resaltar que debe desarrollarse un servicio concurrente: el servidor debe poder atender a nuevas peticiones mientras se está procesando una petición. Por tanto, debe realizarse un servidor con múltiples flujos de ejecución (threads o procesos convencionales).

Como se vio en la parte teórica de la asignatura, generalmente, el desarrollo de un servicio con estado es más sencillo, ya que, básicamente, sólo hace falta que el servidor proporcione unos servicios prácticamente iguales a la interfaz que se le proporciona a la aplicación de usuario. O sea, el servidor ofrecerá un servicio para abrir el directorio, otro para leer una entrada y un tercer servicio que permita cerrar el directorio. Para ello, usará directamente las funciones POSIX para acceso a los directorios (opendir, readdir y closedir).

Se puede decir, por tanto, que el software que se desarrolla permite "conectar" las llamadas de la aplicación con los servicios del sistema operativo remoto.

Como se estudió en la teoría de la asignatura, para construir una arquitectura cliente-servidor, como la que se plantea en esta práctica, usando un mecanismo de comunicación de paso de mensajes, como son los sockets, es necesario establecer qué información viajará entre el cliente y el servidor y viceversa. Deberá, por tanto, asignarse algún tipo de identificador a cada uno de los servicios proporcionados por el servidor, de manera que el cliente pueda especificarlo en sus peticiones, junto con los parámetros correspondientes a dicho servicio. Asimismo, deberá establecerse qué información viaja desde el servidor al cliente como resultado de cada servicio. Esta información debe ser compartida por el cliente y el servidor, para lo que, si es necesario, se usará el fichero de cabecera rdir.h.

Entre las múltiples posibilidades a la hora de implementar este servicio con estado, se deberá usar la que se explica a continuación. La estrategia es hacer que el valor devuelto por la rutina r_opendir sea directamente el que ha retornado la rutina opendir remota, eliminando la necesidad de gestionar algún tipo de descriptor. O sea, una solución en la que el puntero que devuelve la llamada opendir "viaje" como valor devuelto en el servicio de apertura del directorio y se devuelva directamente en la rutina r_opendir. Nótese que, dado que el cliente no realiza ninguna operación sobre ese valor, no deberá aplicársele ningún tipo de transformación sobre el mismo.

En las posteriores llamadas r_readdir y r_closedir la aplicación especificará "ciegamente" ese valor que "viajará" de vuelta en los mensajes correspondientes, de manera que el servidor pueda invocar correctamente las rutinas locales readdir y closedir.

En cualquier caso, el servidor no debe verse afectado por la recepción de valores incorrectos por parte de un cliente erróneo o malintencionado. Así, en el esquema que se debe usar para este servicio y que se acaba de describir, hay que asegurarse de que, aunque el cliente envíe un valor sin sentido como identificador del directorio en los mensajes asociados a r_readdir o r_closedir, el servidor no debe caerse y debe seguir proporcionando servicio. Téngase en cuenta que la llamada readdir, al menos en la implementación de Linux, ante un parámetro erróneo (es decir, que no corresponde a un valor devuelto por un opendir previo), no siempre devuelve un error, sino que puede producir directamente un "Segmentation fault", con la consiguiente caída del servidor.

En último lugar, por si no ha quedado suficientemente claro, es conveniente resaltar en qué consiste el estado para este servicio que se va a implementar. Dado que en la solución propuesta, en principio, no hay que gestionar ninguna estructura en memoria dentro del servidor, puede parecer que no hay estado, pero no es así. Cada llamada a opendir "consume" un descriptor de fichero y memoria dinámica para almacenar el tipo DIR, que incluye entre otras cosas el descriptor de fichero y la posición de lectura actual dentro del directorio. Esos recursos sólo se liberan cuando se produce el closedir correspondiente.

Este estado, además de consumir recursos, proporciona un servicio sin tolerancia a fallos. Así, si el servidor muere y se rearranca, el servidor no podrá servir peticiones readdir y closedir correspondientes a directorios abiertos previamente ya que el directorio ya no está abierto y el parámetro que recibe ya no es significativo.

Desarrollo del servicio sin estado basado en sockets

El desarrollo correcto de este servicio proporcionará una nota de 3,5 puntos.

En primer lugar, hay que resaltar que también en este caso debe desarrollarse un servicio concurrente.

Como se estudió en la parte teórica de la asignatura, en la versión sin estado las peticiones deben ser autocontenidas. Dado que se debe mantener la interfaz de servicio (el modelo de servicio no debe afectar a la aplicación), normalmente, se debe almacenar información en el cliente para lograr este carácter autocontenido. Así, debe ser el cliente el que se "acuerde" de en qué posición del directorio se quedó la última lectura del mismo.

Para implementar esta funcionalidad, puede optar por la solución que considere oportuna. En cualquier caso, a continuación, se proponen dos alternativas:

Asimismo, recuérdese que, aunque los mensajes de petición son más largos debido a su carácter autocontenido, normalmente disminuye el número de mensajes de protocolo. Así, en el caso de la práctica, no es necesario un mensaje para cerrar el directorio, ya que el directorio no permanecerá abierto entre las llamadas. Nótese que una de las "reglas de oro" del servicio sin estado es que el número de recursos usados en el servidor (gasto de memoria, ficheros abiertos, etc) debe permanecer constante entre peticiones: Si una función en el servidor realiza una llamada a opendir, deberá llamar a closedir antes de terminar.

El alumno puede diseñar este servicio como considere oportuno, pero debe tener en cuenta los siguientes aspectos:

Desarrollo del servicio sin estado basado en RPC

El desarrollo correcto de este servicio proporcionará una nota de 3,5 puntos.

En esta tercera versión, se plantea programar una funcionalidad similar a la versión previa, pero usando RPC en vez de sockets. Nótese que en este caso el servidor será secuencial ya que la versión de rpcgen de Linux, al menos que yo sepa, no gestiona automáticamente la concurrencia, a diferencia de, por ejemplo, la versión de Solaris que sí lo hace (para ello, en Solaris se especifica la opción -A: rpcgen -A).

A continuación, se comentan los aspectos específicos en el desarrollo del servicio basado en RPC:

Material de apoyo de la práctica

Al descomprimir el material de apoyo se crea el entorno de desarrollo de la práctica, que reside en el directorio: $HOME/DATSI/SOD/RDIR.2013/.

Como material de apoyo, sólo se proporciona las aplicaciones de ejemplo (rls.c y rls_mix.c) y la interfaz de servicio (dirops.h).

Hay que resaltar que el material de apoyo proporcionado para la versión RPC no compila directamente ya que para ello es necesario que se generen previamente los resguardos de RPC una vez definida la IDL del servicio correspondiente.

Entrega de la práctica

Se realizará en la máquina triqui, usando el mandato:
entrega.sod rdir.2013

Este mandato recogerá los siguientes ficheros: