Pozivi udaljenih procedura

1. Uvod

U prethodnim laboratorijskim vježbama za postizanje komunikacije između međusobno razdvojenih procesa bilo je potrebno poznavanje vrsta usluga koje nudi računalna mreža i API-ja uz pomoć kojega se te usluge mogu koristiti. Taj pristup ima više pozitivnih i negativnih posljedica. Kao prvo, omogućena je veća fleksibilnost a time i iskoristivost/efikasnost u korištenju mrežne usluge, dok sa druge strane to od korisnika zahtijeva da dio vremena potrebnog za razvoj same aplikacije utroši na ostvarivanje komunikacije. Nadalje, prilikom prenošenja podataka mreža nije ulazila u njihovu strukturu, već je te podatke tretirala kao "sirovi" niz okteta za koje je u najboljem slučaju garantirana sama isporuka i očuvan redoslijed. U slučaju heterogene okoline, što je u slučaju računalne mreže pravilo, u kojoj se nalaze računala različite duljine riječi i načina pohranjivanja tih riječi u memoriju korisnik je morao uložiti dodatan napor za prevladavanje tih različitosti.

Logičan nastavak je transparentno korištenje mrežnih usluga pri čemu se korisnika što više odvaja od mrežnih detalja te mu na taj način više vremena ostaje za izradu same aplikacije, a istovremeno se skraćuje razvoj aplikacije koja inherentno ima mogućnost distribuiranog rada. Postoji više različitih načina na koje se taj cilj može ostvariti dok će se u ovoj vježbi obraditi jedan od popularnijih - poziv udaljenih procedura (Remote Procedure Call - RPC).

2. Pozivi udaljenih procedura

Pozivi udaljenih procedura temelje se na klijent/poslužitelj modelu. Pionir u ovom području je Sun Microsystems. Oni su 1988. godine implementirali pozive udaljenih procedura (RPC) u svojem operacijskom sustavu SunOS te isti objavili kao otvoreni standard u nizu RFC-ova. Isti su potom koristili za niz mrežnih usluga od kojih su najpopularijni mrežni informacijski sustav (NIS), i dijeljenje diskova u mrežnom okruženju (NFS). Obje usluge su u međuvremenu dosta unaprijeđene iako se NIS i noviji NIS+ sve brže zamijenjuju LDAP-om, a koriste ih svi proizvođači Unix operacijskih sustava od kojih većina sudjeluje i u razvoju. Osim za Unix postoje i nezavisne implementacije za Windows porodicu. Osim Sun-ove, postojalo je još par implementacija od kojih je zanimljiva DCE-RPC iz 1997. koju je dijelom preuzeo Microsoft, poneštno izmjenio i nadogradio, te prozvao COM/DCOM. Ta implementacija integrirana je u Windows porodicu operacijskih sustava počevši (skromno) sa Windows 95, a vrhunac je doživjela u Windows NT 4. Sam RPC ima ekvivalent i u Javi u vidu RMI-ja. Najnoviji RPC standard ima puni naziv ONC RPC (Open Network Computing RPC) da se što više naglasi njegova otvorenost.

Kako bi se eliminirale razlike koje sa sobom povlači heterogena okolina uveden je univerzalni zapis podataka u vidu XDR-a (External Data Representation) koji je također dokumentiran u RFC-u. Svi podaci neposredno prije prijenosa pretvaraju se u taj format. Za transformaciju primitivnih tipova podataka iz zapisa računala u univerzalni format i obratno postoje funkcije koje se nalaze u paketu sa ostalim funkcijama i programima koji čine kompletni RPC. Ovdje je potrebno naglasiti da ONC RPC i DCE RPC međusobno nisu iteroperabilni.

Osnovna ideja ovog pristupa leži u tome da se pojedine procedure izdvoje iz programa i izvršavaju na zasebnim računalima (iako mogu biti i na lokalnom računalu). Sučelje prema tim procedurama (njihov naziv i ulazni i izlazni parametri) opisuju se u posebnom jeziku. Uz pomoć zasebnog programa (rpcgen) automatski se na osnovu opisanog sučelja generira sav potreban kod za pozivanje udaljene procedure sa programom iz kojeg se ona poziva. Taj kod istovremeno omogućava nezavisnost o transportnom sloju. Rezultat tog pristupa je da su potrebne minimalne izmjene u programu. Ipak postoje neki detalji koje je potrebno poštivati i razumijeti prilikom izrade programa koji koriste RPC.

Sve udaljene procedure grupirane su u programe. Svaki program ima dodijeljenu verziju i jedinstveni broj, a također i svaka procedura unutar programa ima svoju verziju. Prilikom pokretanja poslužioca on registrira svoj jedinstveni broj, verziju te sve procedure i njihove verzije sa tzv. portmapper-om. Portmapper je usluga koja čeka na portu 111 i povezuje klijente sa poslužiteljima. Za pregled programa i procedura koje su registrirane na određenom računalu služi naredba rpcinfo, primjer izvršavanja te naredbe na jednom računalu:

$ /usr/sbin/rpcinfo -p
   program vers proto   port
    100000    2   tcp    111  portmapper
    100000    2   udp    111  portmapper
    100024    1   udp   1024  status
    100024    1   tcp   1024  status
    391002    2   tcp   1025  sgi_fam
 536870913    1   udp   1025
 536870913    1   tcp   1047

Kao što je već rečeno generiranje svog potrebnog koda vezanog za mrežu obavlja program rpcgen. Taj program na osnovu ulaza u kojemu je opisano sučelje poslužitelja generira sav potreban mrežni kod. Primjer opisa jednog sučelja dan je u sljedećem primjeru:

/*
 * dir.x: Remote directory listing protocol
 *
 * This example demostrates the functions of rpcgen
 */

const MAXNAMELEN = 255;                 /* Max. length of directory entry */

typedef string nametype<MAXNAMELEN>;    /* directory entry */
typedef struct namenode *namelist;      /* link in the listing */

/*
 * A node in the directory listing.
 */
struct namenode {
        nametype name;          /* name of direcotry entry */
        namelist next;          /* next entry */
};

/*
 * The result of a READDIR operation
 *
 * a truly portable application would use an agreed upon list of error codes rather
 * than (as this sample program does) rely upon passing UNIX errno's back.
 *
 * In this example: The union is used here to discriminate between successful and
 * unsuccessful remote calls.
 */

union readdir_res switch(int rpcerrno) {
        case 0:
                namelist list;          /* No error: return directory listing */
        default:
                void;                   /* Error occured: nothing else to return */
};

/*
 * The directory program definition
 */
program DIRPROG {
        version DIRVERS {
                readdir_res READDIR(nametype) = 1;
        } = 1;
} = 0x20000076;

Sintaksa opisa sučelja dosta je slična programskom jeziku C. U prethodnom primjeru definirana je funkcija koja vraća sadržaj danog direktorija, a također je definirano da je to prva verzija i programa i funkcije. Jedinstveni broj programa je 0x20000076. Taj broj pripada nerezerviranom području (od 0x200000000 do 0x3fffffff) iz kojega se slobodno može odabrati određen broj no bez garancije da taj broj ne koristi neki drugi program. Ovdje možemo sumirati neke karakteristike tog opisnog jezika te samih procedura:

Kod poslužitelja - bivše lokalne procedure - prikazan je u sljedećem ispisu:

/*
 * dir_proc.c: remote readdir implementation
 */
#include <dirent.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include "dir.h"		/* Created by rpcgen */

extern char *malloc();

readdir_res *readdir_1_svc(nametype *dirname, struct svc_req *req)
{
	DIR *dirp;
	struct dirent *d;
	namelist nl;
	namelist *nlp;
	static readdir_res res;		/* Must be static */

	/*
	 * Open directory
	 */
	dirp = opendir (*dirname);
	if (dirp == (DIR *)NULL) {
		res.rpcerrno = errno;
		return (&res);	
	}

	/*
	 * Free previous result
	 */
	xdr_free (xdr_readdir_res, &res);

	/*
	 * Collect directory entries. Memory allocated here is free by
	 * xdr_free the next time readdir_1 is called.
	 */
	nlp = &res.readdir_res_u.list;
	while (d = readdir(dirp)) {
		nl = *nlp = (namenode *)malloc(sizeof(namenode));
		if (nl == (namenode *)NULL) {
			res.rpcerrno = EAGAIN;
			closedir(dirp);
			return (&res);
		}
		nl->name = strdup(d->d_name);
		nlp = &nl->next;
	}

	*nlp = (namelist)NULL;

	/*
	 * Return the result
	 */
	res.rpcerrno = 0;
	closedir(dirp);
	return (&res);
}

U prethodnom kodu možemo primjetiti par specifičnosti vezanih za izradu poslužitelja:

Konačno, klijent odnosno glavni program ima sljedeći oblik:

/*
 * rls.c: Remote directory listing client
 */

#include 
#include "dir.h"

extern int errno;

main (int argc, char **argv)
{
	CLIENT *clnt;
	char *server;
	char *dir;
	readdir_res *result;
	namelist nl;

	if (argc != 3) {
		fprintf (stderr, "usage: %s  \n", argv[0]);
		exit (1);
	}

	server = argv[1];
	dir = argv[2];

	/*
	 * Create client "handle" used for calling MESSAGEPROG on the server
	 * designated on the command line.
	 */
	clnt = clnt_create (server, DIRPROG, DIRVERS, "tcp");
	if (clnt == (CLIENT *)NULL) {
		clnt_pcreateerror(server);
		exit(1);
	}

	result = readdir_1(&dir, clnt);
	if (result == (readdir_res *)NULL) {
		clnt_perror (clnt, server);
		exit(1);
	}

	/*
	 * Okay, we successfully called the remote procedure.
	 */
	if (result->rpcerrno != 0) {
		/*
		 * Remote system error. Print error message and die.
		 */
		errno = result->rpcerrno;
		perror(dir);
		exit(1);
	}

	/*
	 * Successfully got a directory listing. Print it.
	 */
	for (nl = result->readdir_res_u.list; nl != NULL; nl = nl->next) {
		printf ("%s\n", nl->name);
	}

	xdr_free(xdr_readdir_res, result);
	clnt_destroy(clnt);
	exit(0);
}

Kod klijenta možemo primjetiti sljedeće specifičnosti:

Prevođenje prethodnih programa obavlja se u tri koraka. Prvo se pokretanjem rpcgen-a iz IDL datoteke dobijaju sljedeće četiri datoteke:

Na kraju, potrebno je još prevesti sve u izvršne programe: klijent i poslužitelj. To se obavlja upotrebom sljedećih naredbi:

$ gcc -o dir_proc dir_proc.c dir_svc.c dir_xdr.c
$ gcc -o rls rls.c dir_clnt.c dir_xdr.c

4. Zadatak

Potrebno je ostvariti uslugu ispisa poruke na udaljenom terminalu u mreži, korištenjem poziva udaljene procedure. Parametri poziva klijenta uključuju ime čvora u kojemu je rezidentan poslužilac, poruku koju je potrebno ispisati, te lokalno vrijeme generiranja poziva. Poslužilac treba ispisati poruku sa vremenom generiranja poziva/poruke, te klijentu vratiti odgovor s parametrima koji obuhvaćaju broj ispisanih karaktera poruke i lokalno vrijeme ispisa izvorne poruke. Za dobivanje vremena preporuča se koristiti funkcija time(), ali se može upotrijebiti i bilo koja druga.