UNIVERSIDAD NACIONAL AUTÓNOMA DE NICARAGUA – LEÓN Dpto. Computación Ingeniería Telemática Sistemas Distribuidos
Laboratorio 1: Generación de un programa distribuido con rpcgen. Objetivo Esta práctica tiene como finalidad que el alumno sea capaz de crear un programa distribuido tomando como entrada un programa que originalmente se ejecuta en una máquina local.
Para conseguir este objetivo, se muestra mediante un ejemplo cómo, a partir de un programa local que visualiza la hora del sistema, se determinan qué procedimiento se ejecutarán de manera remota y cuáles de manera local.
Los procedimientos que van a ejecutarse de manera remota van a formar parte de una servicio, que llamaremos rhora. La interfaz del protocolo de este servicio se describe en el lenguaje XDRL, y recibe el nombre de rhora.x
Tras someter dicha interfaz al generador rpcgen y basándonos en el conjunto de ficheros obtenidos en lenguaje C, construiremos un cliente que pida la hora y un servidor que soporte dicho servicio.
Para finalizar el ejemplo, pondremos en ejecución el servidor y, mediante un cliente, comprobaremos comprobaremos que el servidor satisface adecuadamente las peticiones que se le pidan.
La obtención de la hora A continuación c ontinuación vamos a ver, con más detalles, los pasos que se deben seguir para distribuir una aplicación local tomando como ejemplo el programa hora.c 1. Copia, compilación del programa hora.c 2. Elección del conjunto conjunto de procedimientos procedimientos que se situarán en la máquina remota. 3. Creación de una una especificación en lenguaje XDRL del servicio remoto con nombre nombre rhora.x rhora.x 4. Compilación de la interfaz rhora.x para que genere genere los ficheros correspondientes correspondientes en lenguaje C.
1
5. Escrituras de las rutinas de interfaz del lado del cliente y del servidor. 6. Compilación y enlace del programa cliente. 7. Compilación y enlace del programa servidor. 8. Puesta en marcha del servidor y ejecución del cliente.
Paso 1: Copiar, compilar y ejecutar el programa hora.c en el entorno local. Como el objetivo de la práctica es clarificar cómo funciona rpcgen, se ha elegido un programa local muy sencillo que ilustra la mayoría de los puntos de interés de la distribución de una aplicación.
Este programa hora.c visualiza por la salida estándar la hora del sistema en formato “hora:minutos”.
Para generar el ejecutable hora, compilamos y enlazamos con: gcc –o hora hora.c
/*Hora.c*/ #include #include //se definen las variables globales del tiempo struct fecha_t {int anio, mes, dia, hora, minutos;}; struct fecha_t * fechactual; //Declaramos la variable para los segundos transcurridos desde 1970 time_t segundos; int result; int obten_segundos (time_t *s) { //Obtención de los segundos desde el sistema operativo time(s); if (s == NULL) return -1; else return 0; } struct fecha_t * obten_fecha(time_t * s) { struct fecha_t * fecha; //Declaración de un puntero a un objeto con la información del tiempo struct tm *puntero_estructura_tiempo; fecha = (struct fecha_t*) malloc (sizeof (struct fecha_t)); //Actualización del objeto estructura de tiempo con los segundos puntero_estructura_tiempo = localtime(s); //Estracción de la información desde la estructura tiempo fecha->hora = puntero_estructura_tiempo->tm_hour; fecha->minutos = puntero_estructura_tiempo->tm_min; return fecha; }
2
void escribe_hora(struct fecha_t * fecha) { printf("Son las %i:%s%i horas\n", fecha->hora, fecha->minutos <10 ?"0": " ", fecha->minutos); } main() { result = obten_segundos(&segundos); if (result == -1) { printf("Hora.c: error en la llamada time\n"); exit(1); } fechactual=obten_fecha(&segundos); escribe_hora(fechactual); }
Paso 2: Elección de procedimientos que se situarán en la máquina remota. Una vez que la aplicación local funciona correctamente, se puede particionar en componentes remotos y locales. La organización procedural de la aplicación local es la siguiente:
hora.c
main
obten_segundos
obten_fecha
escribe_hora
Máquina Local A la hora de considerar qué procedimientos se pueden mover a una máquina remota, hay que tener en cuenta los recursos que necesitará cada procedimiento. Por ejemplo, escribe_hora accede a al salida estándar del proceso (la pantalla) por lo que este procedimiento debería permanecer local. En general: es difícil mover a una máquina remota procedimientos que realizan operaciones de entrada/salida o manejan descriptores de ficheros locales.
También hay que considerar la ubicación de los datos a los que accede cada procedimiento. Aunque no es éste el caso, podría haber una aplicación que gestionara un diccionario a través de procedimientos, que actuaran sobre una gran base de datos. Parece razonable pensar que si los procedimientos se ubican remotamente, la base de datos debería ubicarse en el mismo servidor que los procedimientos, para evitar transmitir por la red el contenido de la base de datos. En
3
general: los procedimientos deben situarse en la misma máquina donde se ubican los datos sobre los que actúan ya que la transmisión de estructuras grandes de datos como argumentos es ineficaz.
Por último, hay que observar si los procedimientos contienen llamadas al sistema locales. Si se decide que sean remotas y el sistema operativo remoto no soporta la misma interfaz de llamadas, habrá que sustituir las antiguas llamadas locales por las que soporte el nuevo sistema operativo sobre el que actúe el procedimiento.
En este caso, se da esta circunstancia tanto en el procedimiento obten_segundos que invoca a la llamada time y en el procedimiento obten_fecha que usa la llamada local_time.
Como el interés de nuestro programa es obtener una hora fiable de un servidor de tiempos, con hacer remoto el procedimiento obten_segundos es suficiente, ya que la conversión a un formato horario es mejor hacerlo localmente. La máquina destino cuenta con el mismo sistema operativo que la máquina local, por lo que la llamada al sistema time de obten_segundos se puede ejecutar correctamente en la máquina remota. Así, la división de procedimientos entre las dos máquinas quedaría:
rhora.c
main RPC
escribe_hora
obten_fecha
rhora_srp.c obten se undos
Máquina Local
Máquina Remota
Una vez realizada la división de los procedimientos, el siguiente paso es dividir el programa inicial hora.c en dos componentes. Se seleccionan las constantes y estructuras de datos que usará cada parte situándolas en ficheros separados. En la máquina local, el fichero rhora.c contendrá exactamente lo mimo que el programa local hora.c excepto la declaración y el cuerpo del procedimiento remoto obten_segundos.
El fichero rhora_srp.c (source remote procedure) contiene la declaración y el cuerpo del procedimiento que obtiene los segundos del sist ema.
4
Paso 3: Especificación en lenguaje XDRL del servicio rhora.x Una vez que el programador selecciona el conjunto de procedimientos que se ejecutarán remotamente, ha que preparar la especificación de la interfaz, que recogerá dichos procedimientos, en un lenguaje que entienda el compilador de interfaces rpcgen. Este lenguaje, que no es C aunque se le parece bastante, se llama XDRL.
La especificación contendrá las declaraciones de constantes, tipos de datos y perfiles de los procedimientos que el servidor ofrecerá a los clientes.
A un fichero que contiene la especificación de un servicio en lenguaje XDRL se la asigna la extensión .x
Así, el fichero rhora.x recoge la especificación del servicio de hora remota.
/*rhora.x*/ enum horastat{ HORA_OK = 0, /*no error*/ HORA_NOK = 1 /*error*/ }; /*valor de la hora*/ union segactual switch(horastat status){ case HORA_OK: long segundos; default: void; }; /*Procedimiento remoto de la hora*/ program HORA_PROGRAM{ version HORA_VERSION{ segactual OBTEN_SEGUNDOS(void) = 1; }=1; }=0x30; /*Aquí se pone un valor distinto por el alumno*/
Por convenio se usan letras mayúsculas para representar los nombres de procedimientos y del programa.
Hay que destacar que el procedimiento OBTEN_SEGUNDOS se ha diseñado para que solo tenga un parámetro de salido. Y ese parámetro es un registro variable que, dependiendo del valor de la etiqueta status, devolverá un valor u otro: •
status = HORA_OK indica que el servidor ha realizado el servicio con éxito y, por tanto, el campo segundos contendrá los segundos pedidos.
5
•
status
= HORA_NOK indica que se ha producido un error en la ejecución del
procedimiento remoto, por lo que no habrá información en el registro.
Por último, el número del programa asignado al servidor debe ser único para cada programa. De esta forma podemos tener varios servidores corriendo a la vez en el mismo ordenador y los clientes tendrán una forma de referenciarlos. Si se usara el mismo número, solo podría registrarse un servidor (el primero en arrancarse) y no será posible que cada alumno depurara su propio servidor.
Paso 4: Compilación de la interfaz rhora.x Una vez modificado adecuadamente el número de programa en rhora.x se le someterá al compilador de interfaces rpcgen: rpcgen –C rhora.x
La opción –C genera código compatible con ANSI C o C estándar.
Esta compilación comprueba errores sintácticos y, si nos los encuentra, genera los siguientes ficheros en C: •
rhora_clnt.c: stubs del cliente
•
rhora_svc,c: stub del servidor
•
rhora_xdr.c: procedimientos que convierten a/desde formato local desde/a formato externo los tipos de datos declarados en rhora.x
•
rhora.h: declaraciones de constantes y tipos en c contenidos en el fichero de especificación, así como los perfiles de la función obten_segundos para el cliente y el servidor.
Paso 5: Escritura de las rutinas de interfaz Como ya se ha comentado, en el lado del cliente seguirá estando el programa principal rhora.c. Este programa llama al procedimiento obten_segundos con el mismo parámetro. No hay que olvidar que en el paso 2 hemos s ituado el cuerpo de este procedimiento en la máquina remota.
Por otra parte, como resultado de someter la especificación del servicio rhora.x al compilador rpcgen se obtiene, entre otros, el stub del lado del cliente del procedimiento obten_segundos_1 (fichero rhora_clnt.c). Este procedimiento es el que realiza desde el lado del cliente la llamada al procedimiento remoto que devuelve los segundos pedidos. Por tanto, desde el programa principal, sin modificar el código original, se tendría que invocar el procedimiento obten_segundos_1. Pero
6
no es posible, ya que el procedimiento llamado desde el programa principal no concuerda con el ofrecido por el stub pues obten_segundos y obten_segundos_1 no se llaman igual. Obsérvese que el procedimiento generado por rpcgen acaba con el sufijo _1. Este sufijo indica la versión 1 del servicio. El problema no acaba aquí, no solo no se llaman igual, sino que además tienen diferentes tipos de parámetros como se muestra a continuación:
//declaración de obten_segundos en rhora.c int obten_segundos (time_t *s); //declaración de obten_segundos_1 en rhora_clnt.c segactual * obten_segundos_1(void *argp, CLIENT *clnt);
Hay una solución: escribir en la máquina local, el cuerpo del procedimiento obten_segundos para sea éste el que llame al procedimiento del stub obten_segundos_1 con los parámetros adecuados y recoja el resultado de obten_segundos_1 para devolverlo al programa principal, de acuerdo al tipo de resultado que éste espera. Es decir, todo este proceso de adaptación de los parámetros entre el programa principal y el procedimiento del stub del cliente obten_segundos_1 se recoge dentro del procedimiento obten_segundos.
Por convenio, este procedimiento se conoce como rutina de interfaz del cliente y le vamos a incluir en el fichero rhora_cif.c (client interfaces).
Gráficamente, el esquema de llamada es:
rhora.c
main() { ……… obten_segundos(); ……… }
rhora_cif.c
…. int obten_segundos(time_t *s) { prepara_parámetros obten_segundos_1(parámetros) recoge resultados }
rhora_clnt.c
segactual * obten_segundos_1(void *argp, CLIENT *clnt) { ….. Aquí se hace la RPC
7
Además, para que esta rutina de interfaz sea capaz de realizar una llamada remota al servidor es necesario crear un cliente del servicio de tiempo con la rutina clnt_create que ofrece la librería rpc. Por eso es necesario hacer #include .
Los parámetros que se necesitan para crear un cliente son: el nombre de la máquina donde reside el servidor de hora, el nombre de programa del servicio HORA_PROGRAM y HORA_VERSION y el protocolo de comunicación. Nosotros utilizaremos udp.
Con todas estas consideraciones, el código de la runita de la interfaz del cliente podría ser el siguiente:
//rhora_cif.c #include #include #include "rhora.h" #define Rmaquina "localhost" static CLIENT *clte_rhora; int obten_segundos(time_t *s) { struct segactual *segundos; //se crea un cliente del servicio de hora clte_rhora = clnt_create(Rmaquina, HORA_PROGRAM, HORA_VERSION, "udp"); if (clte_rhora == NULL) return -1; segundos = (struct segactual*) malloc (sizeof (struct segactual)); //se realiza la rpc segundos = obten_segundos_1(NULL, clte_rhora); if (segundos == NULL) //error de comunicación con el servidor return -1; else if (segundos->status == HORA_NOK) //error de ejecución return -1; else // se recogen los segundo devueltos *s = (time_t)segundos->segactual_u.segundos; }
Con el procedimiento anterior hay que destacar que cuando hay un error en la conexión o bien en el servidor devuelve un error, el procedimiento enmascara el error de comunicación devolviendo un -1, para que el programa principal siga viendo la llamada si se ejecutara localmente.
Rutinas de interfaz del lado del servidor: rhora_sif.c
En el lado del servidor ocurre algo similar a lo que ocurre en el lado del cliente. El servidor, que ejecuta el programa rhora_svc.c, se encarga de recibir peticiones procedentes de los clientes, analizar la petición a realizar, e invocar el procedimiento que lleva a cabo dicha petición.
8
En nuestro caso, cuando el servidor reciba la petición de obtención de la hora en segundos, éste invoca el procedimiento del stub obten_segunods_1_svc. Como se puede observar, surge el mismo problema que apareció en el apartado anterior: este procedimiento no se llama igual que el procedimiento obten_segundos (que incluimos en el paso 2 en el fichero rhora_srp.c) ni cuenta con los mismos parámetros.
//declaración de obten_segundos en rhora_srp.c int obten_segundos (time_t *s); //declaración de obten_segundos_1_svc en rhora_clnt.c segactual * obten_segundos_1_svc(void *p1, struct svc_req *p2);
La solución se basa en usar la misma idea aplicada en el apartado anterior: hay que escribir un cuerpo del procedimiento obten_segundos_1_svc que invoque adecuadamente al procedimiento obten_segundos, recoja el valor devuelto por este procedimiento, lo analice y, finalmente construya una respuesta que esté de acuerdo con lo que espera el programa rhora_svc.c
La declaración y el cuerpo de este procedimiento debe guardarse en el fichero rhora_sif.c (Server interface file). Gráficamente:
rhora_svc.c
main() { ……… obten_segundos_1_svc(); ……… }
rhora_sif.c
segactual * obten_segundos_1_svc(…) { prepara_parámetros obten_segundos(..) recoge resultados
}
rhora_srp.c
int obten_segundos_1_svc (time_t *s) { Aquí se hace la llamada local
Time(&s); }
9
El código de la rutina de la interfaz del servidor podría ser el siguiente:
//rhora_sif.c #include #include #include #include #include "rhora.h" extern segactual * obten_segundos_1_svc(void *p1, struct svc_req *p2) { int resultado; time_t s; segactual *segundos_actuales; //se reserva espacio para almacenar el resultado segundos_actuales = (segactual *) malloc (sizeof (struct segactual)); //Se llama al procedimiento local resultado = obten_segundos(&s); if (resultado == -1) //error de ejecución del procedimiento local segundos_actuales->status = HORA_NOK; else //el procedimiento local se ejecutó correctamente { segundos_actuales->status = HORA_OK; segundos_actuales->segactual_u.segundos = s; } return segundos_actuales; }
Paso 6: Compilación y enlace del programa cliente rhora Se compilan los siguientes ficheros: gcc –c rhora_cif.c gcc –c rhora.c gcc –c rhora_clnt.c gcc –c rhora_xdr.c
Se enlazan todos estos ficheros en un solo ejecutable que será el cliente rhora: gcc –o rhora rhora.o rhora_cif.o rhora_clnt.o rhora_xdr.o
El diagrama de fichero que se usan para generar el programa del cliente es el siguiente:
rhora_clnt.c
rhora.x
rpcgen
rhora.h
rhora_xdr.c
rhora.c
rhora_cif.c
Compilador C
rhora
10
Paso 7: Compilación y enlace del programa servidor rhorad Se compilan los siguientes ficheros: gcc –c rhora_sif.c gcc –c rhora_srp.c gcc –c rhora_svc.c gcc –c rhora_xdr.c
Se enlazan todos estos ficheros en un solo ejecutable que será el servidor rhorad: gcc –o rhorad rhora_svc.o rhora_sif.o rhora_srp.o rhora_xdr.o
El diagrama de fichero que se usan para generar el programa del servidor es el siguiente:
rhora_svc.c
rhora.x
rhora_srp.c
rhora.h
rpcgen
rhora_sif.c
Compilador C
rhora_xdr.c
rhorad
Paso 8: Puesta en marcha del servidor y ejecución del cliente El servidor debe empezar a ejecutarse antes que el cliente, para evitar errores como este: Servidor: RCP: Program not registered
En un sistema Unix se arranca el servidor con el siguiente comando: ./rhorad &
Este comando arranca el servidor en un segundo plano (background) y deja el Terminal libre para suministrarle otros comandos. Hay que anotar el número del proceso pid que aparece, por si queremos matar al servidor con el comando: kill -9 pid
11
Trabajo asignado: 1. Ejecute en primer lugar el programa hora.c en un entorno local. 2. Siga cada uno de los pasos descritos en esta práctica, para convertir al programa hora en una aplicación distribuida. 3. Pruebe en primer lugar el cliente y el servidor en la misma máquina y luego en máquinas diferentes. 4. Cree un fichero Makefile, para hacer menos pesada la organización de los ficheros que intervienen en la aplicación distribuida. Pruebe este fichero.
Plazo de realización: Dos sesiones de Laboratorio.
12