No existe, sin embargo, mucha documentación sobre cómo programar usando este mecanismo (en mi opinión, la mejor referencia es el capítulo 16 del libro de Richard Stevens UNIX Network Programming, Volume 2, Second Edition: Interprocess Communications, Prentice Hall, 1999, ISBN 0-13-081081-9). Este documento nace, por tanto, con el objetivo de proporcionar una guía rápida para la programación en este entorno, de manera que no se requiera consultar ningún documento adicional a la hora de acometer las prácticas de la asignatura que usan esta tecnología.
Dado el carácter aplicado que pretende tener esta guía, la presentación se realizará usando programas de ejemplo de progresiva dificultad (este enlace permite la descarga de los programas de ejemplo):
program RUSUARIOS_SERVICIO { version RUSUARIOS_VERSION { /* pendiente de rellenar */ }=1; }=666666666;
Observe el uso de mayúsculas en los nombres de los identificadores. Esta convención reduce la posibilidad de conflictos entre los nombres generados automáticamente por el sistema de RPCs y los que usa la aplicación.
El próximo paso sería declarar los servicios que proporcionará el servidor. El siguiente listado (fichero rusuarios.x del directorio caso1_erroneo/idl) muestra esta declaración, donde nuevamente se han usado nombres en mayúsculas por los motivos antes explicados y se le han asignado valores enteros sucesivos a partir de 1 a cada función (el 0 está reservado para un servicio nulo que incluye automáticamente el sistema de RPCs en cada servidor).
program RUSUARIOS_SERVICIO { version RUSUARIOS_VERSION { int OBTENER_UID(string nombre)=1; string OBTENER_NOMBRE(int uid)=2; }=1; }=666666666;
$ rpcgen rusuarios.x $ ls rusuarios_clnt.c rusuarios.h rusuarios_svc.c rusuarios.x
$ rpcgen -Ss rusuarios.x > ../servidor/rusuariosd.c
#include "rusuarios.h" int * obtener_uid_1_svc(char **argp, struct svc_req *rqstp) { static int result; /* * insert server code here */ return &result; } char ** obtener_nombre_1_svc(int *argp, struct svc_req *rqstp) { static char * result; /* * insert server code here */ return &result; }
#include <string.h> #include <pwd.h> #include <sys/types.h> #include "rusuarios.h" int * obtener_uid_1_svc(char **argp, struct svc_req *rqstp) { static int result; struct passwd *desc_usuario; result=-1; desc_usuario=getpwnam(*argp); if (desc_usuario) result=desc_usuario->pw_uid; return &result; } char ** obtener_nombre_1_svc(int *argp, struct svc_req *rqstp) { static char * result; struct passwd *desc_usuario; desc_usuario=getpwuid(*argp); if (desc_usuario) result=desc_usuario->pw_name; else result=NULL; return &result; }
En este punto, se puede compilar y ejecutar el servidor. Cuando ya esté arrancado, puede usar el mandato rpcinfo para ver cómo el servidor se ha dado de alta en el portmap (o binder):
$ rpcinfo -p programa vers proto puerto ........................... 666666666 1 udp 60659 666666666 1 tcp 60674 ...........................
$ rpcinfo -t localhost 666666666 1 el programa 666666666 versión 1 está listo y a la espera $ rpcinfo -u localhost 666666666 1 el programa 666666666 versión 1 está listo y a la espera
Tomando como punto de partida ese código de muestra, se han desarrollado dos clientes, cada uno de los cuales usa uno de los servicios implementados.
El cliente getuid.c hace uso del servicio que retorna el uid dado un nombre de usuario para obtener esa información de todos los nombres de usuario que recibe como argumentos de la línea de mandatos (excepto argv[1] donde se especificará la máquina donde ejecuta el servidor).
#include <stdio.h> #include "rusuarios.h" int main (int argc, char *argv[]) { CLIENT *clnt; // PUNTO 1 int *result; int i; if (argc < 3) { printf ("usage: %s server_host nombre...\n", argv[0]); exit (1); } // PUNTO 2 clnt = clnt_create (argv[1], RUSUARIOS_SERVICIO, RUSUARIOS_VERSION, "tcp"); if (clnt == NULL) { // PUNTO 3 clnt_pcreateerror (argv[1]); exit (1); } for (i=2; i<argc; i++) { result = obtener_uid_1(&argv[i], clnt); // PUNTO 4 if (result == NULL) // PUNTO 5 clnt_perror (clnt, "error en la llamada"); else if (*result == -1) // PUNTO 6 printf("%s: no existe ese usuario\n", argv[i]); else printf("%s: %d\n", argv[i], *result); } clnt_destroy (clnt); // PUNTO 7 exit (0); }
$ ./getuid localhost root nadie fperez root: 0 nadie: no existe ese usuario fperez: 1001
#include <stdio.h> #include <stdlib.h> #include "rusuarios.h" int main (int argc, char *argv[]) { CLIENT *clnt; char **result; int i; int uid; if (argc < 3) { printf ("usage: %s server_host uid...\n", argv[0]); exit (1); } clnt = clnt_create (argv[1], RUSUARIOS_SERVICIO, RUSUARIOS_VERSION, "tcp"); if (clnt == NULL) { clnt_pcreateerror (argv[1]); exit (1); } for (i=2; i<argc; i++) { uid=atoi(argv[i]); result = obtener_nombre_1(&uid, clnt); if (result == NULL) clnt_perror (clnt, "error en la llamada"); else if (*result) printf("%d: %s\n", uid, *result); else printf("%d: no existe ese UID\n", uid); } clnt_destroy (clnt); exit (0); }
Intentemos ejecutar este programa cliente:
$ ./getname localhost 0 877 1001 0: root error en la llamada: RPC: Can't decode result 1001: fperez
El problema está en el manejo del tipo string de XDR. Cuando se usa este tipo en las declaraciones de un fichero XDR (ya sea como argumento de una función, como valor retornado, o como un campo de una estructura), hay que asegurarse en el código del cliente y/o en el del servidor de que una variable de este tipo siempre apunta a un string válido, es decir, a una cadena de 0 ó más caracteres que termina con un carácter nulo (\0) adicional.
En el ejemplo, cuando el servidor, dentro del servicio que obtiene el nombre de usuario asociado a un uid, encuentra que ese identificador de usuario no existe, realiza la siguiente asignación:
result=NULL;
result="";
result = obtener_nombre_1(&uid, clnt); if (result == NULL) clnt_perror (clnt, "error en la llamada"); else if (strlen(*result)!=0) printf("%d: %s\n", uid, *result); else printf("%d: no existe ese UID\n", uid);
Con esa modificación, el servicio parece funcionar correctamente. Sin embargo, hay un error latente en el mismo.
Si revisamos el manual de la función getpwuid, observamos que explica que una llamada a esta función puede sobreescribir los datos devueltos por una llamada previa. Para entenderlo mejor, podemos usar el siguiente ejemplo, que, obviamente, no tiene nada que ver con las RPC.
#include <stdio.h> #include <pwd.h> int main() { struct passwd *desc_usuario; desc_usuario=getpwuid(0); getpwuid(1); printf("UID 0: %s\n", desc_usuario->pw_name); return 0; }
En la función de servicio desarrollada (obtener_nombre_1_svc) sólo hay una llamada a getpwuid, pero el propio resguardo del servidor podría también hacer esa llamada, sobreescribiendo los datos obtenidos. Por tanto, lo más razonable es crear una copia en el heap:
if (desc_usuario) result=strdup(desc_usuario->pw_name);
char ** obtener_nombre_1_svc(int *argp, struct svc_req *rqstp) { static char * result; struct passwd *desc_usuario; xdr_free((xdrproc_t)xdr_string, (char *)&result); ...................................
Falta un último detalle para completar una solución válida. Al usar xdr_free al principio de la función obtener_nombre_1_svc, hay que asegurarse de que la variable result siempre hace referencia a información almacenada en el heap. Eso no ocurre si en la invocación previa el uid no existía, ya que en ese caso result apuntará a un literal vacío de tipo string ("") almacenado de forma estática.
Para resolver este problema, se puede usar strdup también en el caso de que el uid no exista:
result=strdup("");
char ** obtener_nombre_1_svc(int *argp, struct svc_req *rqstp) { static char * result; struct passwd *desc_usuario; xdr_free((xdrproc_t)xdr_string, (char *)&result); desc_usuario=getpwuid(*argp); if (desc_usuario) result=strdup(desc_usuario->pw_name); else result=strdup(""); return &result; }
Para practicar con esta característica, vamos a extender el servicio desarrollado como ejemplo incluyendo dos nuevas funciones de servicio:
struct pareja_uids { int uid1; int uid2; }; struct respuesta_ugid { int uid; int gid; }; program RUSUARIOS_SERVICIO { version RUSUARIOS_VERSION { int OBTENER_UID(string nombre)=1; string OBTENER_NOMBRE(int uid)=2; bool MISMO_GID(pareja_uids)=3; respuesta_ugid OBTENER_UGID(string nombre)=4; }=1; }=666666666;
Si aplicamos el mandato rpcgen a este fichero IDL, podemos comprobar que aparece un nuevo tipo de fichero (rusuarios_xdr.c):
$ rpcgen rusuarios.x $ ls rusuarios_clnt.c rusuarios.h rusuarios_svc.c rusuarios.x rusuarios_xdr.c
El siguiente listado muestra el código del servidor (fichero caso2/servidor/rusuariosd.c), incluyendo únicamente la parte correspondiente a los dos nuevos servicios):
#include <string.h> #include <pwd.h> #include <sys/types.h> #include "rusuarios.h" bool_t * mismo_gid_1_svc(pareja_uids *argp, struct svc_req *rqstp) { static bool_t result; struct passwd *desc_usuario; int gid1, gid2; result=FALSE; desc_usuario=getpwuid(argp->uid1); if (desc_usuario) { gid1=desc_usuario->pw_gid; desc_usuario=getpwuid(argp->uid2); if (desc_usuario) { gid2=desc_usuario->pw_gid; result=(gid1==gid2); } } return &result; } respuesta_ugid * obtener_ugid_1_svc(char **argp, struct svc_req *rqstp) { static respuesta_ugid result; struct passwd *desc_usuario; result.uid=-1; result.gid=-1; desc_usuario=getpwnam(*argp); if (desc_usuario) { result.uid=desc_usuario->pw_uid; result.gid=desc_usuario->pw_gid; } return &result; }
A continuación, se muestra un programa cliente (fichero caso2/cliente/getugid.c) del servicio que devuelve el uid y gid de un usuario.
#include <stdio.h> #include "rusuarios.h" int main (int argc, char *argv[]) { CLIENT *clnt; respuesta_ugid *result; int i; if (argc < 3) { printf ("usage: %s server_host nombre...\n", argv[0]); exit (1); } clnt = clnt_create (argv[1], RUSUARIOS_SERVICIO, RUSUARIOS_VERSION, "tcp"); if (clnt == NULL) { clnt_pcreateerror (argv[1]); exit (1); } for (i=2; i<argc; i++) { result = obtener_ugid_1(&argv[i], clnt); if (result == NULL) clnt_perror (clnt, "error en la llamada"); else if ((result->uid == -1)||(result->gid == -1)) printf("%s: no existe ese usuario\n", argv[i]); else printf("%s: UID %d GID %d\n", argv[i], result->uid, result->gid); } clnt_destroy (clnt); exit (0); }
#include <stdio.h> #include "rusuarios.h" int imprime_nombre(int uid, CLIENT *cl) { int ok=FALSE; char **result_nombre; result_nombre = obtener_nombre_1(&uid, cl); if (result_nombre == NULL) clnt_perror (cl, "error en la llamada"); else if (strlen(*result_nombre) == 0) printf("%d: no existe ese usuario\n", uid); else { ok=TRUE; printf("(%s:%d)", *result_nombre, uid); } return ok; } int main (int argc, char *argv[]) { CLIENT *clnt; int uid1, uid2; pareja_uids mismo_gid_arg; bool_t *result_mismo; int i, j; if (argc < 3) { printf ("usage: %s server_host uid...\n", argv[0]); exit (1); } clnt = clnt_create (argv[1], RUSUARIOS_SERVICIO, RUSUARIOS_VERSION, "tcp"); if (clnt == NULL) { clnt_pcreateerror (argv[1]); exit (1); } for (i=2; i<argc; i++) { if (argv[i]) { uid1=atoi(argv[i]); if (!imprime_nombre(uid1, clnt)) continue; mismo_gid_arg.uid1=uid1; for (j=i+1; j<argc; j++) { if (argv[j]) { uid2=atoi(argv[j]); mismo_gid_arg.uid2=uid2; result_mismo=mismo_gid_1(&mismo_gid_arg,clnt); if (result_mismo == NULL) clnt_perror (clnt, "error en la llamada"); else if (*result_mismo) { imprime_nombre(uid2, clnt); argv[j]=NULL; } } } printf("\n"); } } clnt_destroy (clnt); exit (0); }
En esta sección, se plantea una solución alternativa basada en el tipo union de XDR, que es distinto del tipo union de C. El tipo de XDR se parece más a los registros variantes presentes en lenguajes como Ada, en los que hay un campo discriminante que determina cuál es el contenido del registro en cada caso. Esto nos va a permitir también analizar cómo se manejan los tipos XDR que no corresponden directamente a tipos de C.
A continuación, se incluye una versión del fichero IDL (caso3/idl/rusuarios.x) que usa este tipo de datos específico de XDR.
union respuesta_nombre switch(bool existe){ case FALSE: void; case TRUE: string nombre<>; }; program RUSUARIOS_SERVICIO { version RUSUARIOS_VERSION { int OBTENER_UID(string nombre)=1; respuesta_nombre OBTENER_NOMBRE(int uid)=2; bool MISMO_GID(pareja_uids)=3; respuesta_ugid OBTENER_UGID(string nombre)=4; }=1; }=666666666;
A la hora de programar el servidor y los posibles clientes, es necesario averiguar cómo se ve el tipo union de XDR desde el código C. Para ello, es recomendable editar el fichero de cabecera (.h), generado automáticamente por rpcgen, para poder ver esa correspondencia.
A continuación, se muestra el fragmento del fichero rusuarios.h que contiene la definición que nos interesa:
struct respuesta_nombre { bool_t existe; union { char *nombre; } respuesta_nombre_u; };
El siguiente listado muestra el código del servidor (fichero caso3/servidor/rusuariosd.c), incluyendo únicamente la parte correspondiente al servicio obtener_nombre_1_svc):
respuesta_nombre * obtener_nombre_1_svc(int *argp, struct svc_req *rqstp) { static respuesta_nombre result; struct passwd *desc_usuario; xdr_free((xdrproc_t)xdr_respuesta_nombre, (char *)&result); desc_usuario=getpwuid(*argp); if (desc_usuario) { result.existe=TRUE; result.respuesta_nombre_u.nombre=strdup(desc_usuario->pw_name); } else result.existe=FALSE; return &result; }
A continuación, se lista el cliente afectado por el cambio (fichero caso3/cliente/getname.c).
#include <stdio.h> #include <stdlib.h> #include "rusuarios.h" int main (int argc, char *argv[]) { CLIENT *clnt; respuesta_nombre *result; int i; int uid; if (argc < 3) { printf ("usage: %s server_host uid...\n", argv[0]); exit (1); } clnt = clnt_create (argv[1], RUSUARIOS_SERVICIO, RUSUARIOS_VERSION, "tcp"); if (clnt == NULL) { clnt_pcreateerror (argv[1]); exit (1); } for (i=2; i<argc; i++) { uid=atoi(argv[i]); result = obtener_nombre_1(&uid, clnt); if (result == NULL) clnt_perror (clnt, "error en la llamada"); else if (result->existe == FALSE) printf("%d: no existe ese UID\n", uid); else printf("%d: %s\n", uid, result->respuesta_nombre_u.nombre); } clnt_destroy (clnt); exit (0); }
Para ilustrar este mecanismo, vamos a extender el servicio con una nueva función que recibe una lista de uids y retorna el conjunto de gids correspondientes. A continuación, se incluye el nuevo fichero IDL (caso4/idl/rusuarios.x) que usa vectores de tamaño variable.
struct pareja_uids { int uid1; int uid2; }; struct respuesta_ugid { int uid; int gid; }; union respuesta_nombre switch(bool existe){ case FALSE: void; case TRUE: string nombre<>; }; struct arg_gids { int uids<>; }; struct respuesta_gids { int gids<>; }; program RUSUARIOS_SERVICIO { version RUSUARIOS_VERSION { int OBTENER_UID(string nombre)=1; respuesta_nombre OBTENER_NOMBRE(int uid)=2; bool MISMO_GID(pareja_uids)=3; respuesta_ugid OBTENER_UGID(string nombre)=4; respuesta_gids OBTENER_GIDS(arg_gids lista_uids)=5 }=1; }=666666666;
A continuación, se muestra el fragmento del fichero rusuarios.h que contiene las definiciones que nos interesan:
struct arg_gids { struct { u_int uids_len; int *uids_val; } uids; }; typedef struct arg_gids arg_gids; struct respuesta_gids { struct { u_int gids_len; int *gids_val; } gids; }; typedef struct respuesta_gids respuesta_gids;
El siguiente listado muestra el código del servidor (fichero caso4/servidor/rusuariosd.c), incluyendo únicamente la parte correspondiente al nuevo servicio:
respuesta_gids * obtener_gids_1_svc(arg_gids *argp, struct svc_req *rqstp) { static respuesta_gids result; struct passwd *desc_usuario; int tam, i; xdr_free((xdrproc_t)xdr_respuesta_gids, (char *)&result); tam= argp->uids.uids_len; result.gids.gids_val=malloc(sizeof(int) * tam); result.gids.gids_len=tam; for (i=0; i<tam; i++) { desc_usuario=getpwuid(argp->uids.uids_val[i]); if (desc_usuario) result.gids.gids_val[i]=desc_usuario->pw_gid; else result.gids.gids_val[i]=-1; } return &result; }
A continuación, se muestra un programa cliente (fichero caso4/cliente/getgids.c) del nuevo servicio.
#include <stdio.h> #include "rusuarios.h" int main (int argc, char *argv[]) { CLIENT *clnt; respuesta_gids *result; arg_gids arg; int i, tam; if (argc < 3) { printf ("usage: %s server_host uid...\n", argv[0]); exit (1); } clnt = clnt_create (argv[1], RUSUARIOS_SERVICIO, RUSUARIOS_VERSION, "tcp"); if (clnt == NULL) { clnt_pcreateerror (argv[1]); exit (1); } tam=argc-2; arg.uids.uids_len=tam; arg.uids.uids_val=malloc(tam*sizeof(int)); for (i=0; i<tam; i++) arg.uids.uids_val[i]=atoi(argv[i+2]); result = obtener_gids_1(&arg, clnt); if (result == NULL) clnt_perror (clnt, "error en la llamada"); else { tam=result->gids.gids_len; for (i=0; i<tam; i++) if (result->gids.gids_val[i] == -1) printf("%d: no existe ese usuario\n", arg.uids.uids_val[i]); else printf("UID %d: GID %d\n", arg.uids.uids_val[i], result->gids.gids_val[i]); } clnt_destroy (clnt); exit (0); }
En el caso de que se requiera enviar una estructura de datos compleja basada en punteros, como, por ejemplo, una lista enlazada, el emisor debe enviar cada nodo de la lista, junto con la información adicional necesaria para poder reconstruir la lista en el receptor.
El sistema RPC/ONC se encarga de realizar automáticamente todo este trabajo de serialización y deserialización de las estructuras de datos basadas en punteros lo que permite que el programador gestione este tipo de estructuras de la misma forma que en un programa convencional.
Para mostrar cómo se manejan los punteros en este sistema, vamos a extender el servicio con una nueva función que obtenga el nombre de todos los usuarios que existen en el sistema remoto, retornándolos en una lista enlazada. A continuación, se incluye el nuevo fichero IDL (caso5/idl/rusuarios.x) en el que se puede apreciar que se definen dos nuevos tipos (usuario y respuesta_usuarios) que contienen internamente campos de tipo puntero (sig y lista, respectivamente) que permiten retornar una lista como resultado de una función de servicio.
struct pareja_uids { int uid1; int uid2; }; struct respuesta_ugid { int uid; int gid; }; union respuesta_nombre switch(bool existe){ case FALSE: void; case TRUE: string nombre<>; }; struct arg_gids { int uids<>; }; struct respuesta_gids { int gids<>; }; struct usuario { string nombre<>; struct usuario *sig; }; struct respuesta_usuarios { struct usuario *lista; }; program RUSUARIOS_SERVICIO { version RUSUARIOS_VERSION { int OBTENER_UID(string nombre)=1; respuesta_nombre OBTENER_NOMBRE(int uid)=2; bool MISMO_GID(pareja_uids)=3; respuesta_ugid OBTENER_UGID(string nombre)=4; respuesta_gids OBTENER_GIDS(arg_gids lista_uids)=5 respuesta_usuarios OBTENER_USUARIOS(void)=6; }=1; }=666666666;
El siguiente listado muestra el código del servidor (fichero caso5/servidor/rusuariosd.c), incluyendo únicamente la parte correspondiente al nuevo servicio:
respuesta_usuarios * obtener_usuarios_1_svc(void *argp, struct svc_req *rqstp) { static respuesta_usuarios result; struct passwd *desc_usuario; struct usuario *nuevo; xdr_free((xdrproc_t)xdr_respuesta_usuarios, (char *)&result); result.lista = NULL; while ((desc_usuario=getpwent())) { nuevo = malloc(sizeof(usuario)); nuevo->nombre = strdup(desc_usuario->pw_name); nuevo->sig = result.lista; result.lista = nuevo; } endpwent(); return &result; }
A continuación, se muestra un programa cliente (fichero caso5/cliente/getusers.c) del nuevo servicio.
#include <stdio.h> #include <stdlib.h> #include "rusuarios.h" int main (int argc, char *argv[]) { CLIENT *clnt; respuesta_usuarios *result; void *dummy=NULL; if (argc != 2) { printf ("usage: %s server_host\n", argv[0]); exit (1); } clnt = clnt_create (argv[1], RUSUARIOS_SERVICIO, RUSUARIOS_VERSION, "tcp"); if (clnt == NULL) { clnt_pcreateerror (argv[1]); exit (1); } result = obtener_usuarios_1(dummy, clnt); if (result == NULL) clnt_perror (clnt, "error en la llamada"); else { struct usuario *lista = result->lista; for (;lista; lista=lista->sig) printf("%s\n", lista->nombre); } clnt_destroy (clnt); exit (0); }