Távoli eljáráshívás Összeállította: Cser András,Wolkensdorfer Péter, Mohácsi János Irányítástechnika és Informatika Tanszék
Távoli eljáráshívás 83 1. ÁLTALÁNOS SZÁMÍTÓGÉPHÁLÓZATI ALAPISMERETEK 1.1. Bevezetés 1.1.1. Áttekintés A távoli eljáráshívás (Remote Procedure Call, RPC) egy olyan magasszintû kommunikációs paradigma, amely lehetôvé teszi, hogy távoli eljárások meghívása takarja el az adott alkalmazásban az alsóbb hálózati rétegeket. Az RPC egy logikus kliens-szerver kommunikációt implementál, amely különösen alkalmas hálózatos alkalmazások megvalósítására. Az RPC-ben a kliens eljárás igényeket küld a szervernek, amely azokat megérkezésük után adminisztrálja, elvégzi a kért funkciót, viszontválaszt küld és az eljáráshívás visszatér a kliensnek. Hogyan lehet mindezt használni? Az RPC-vel történô programozás elsôdleges hatása, hogy a programok egy hálózati kliensszerver modellen belül futnak, anélkül, hogy ismerniük kellene az alsóbb hálózati rétegeket és azok mûködését. Az RPC leveszi a programozó hátáról a rabszolgamunka nagy részét, azáltal, hogy a hívások transzparensek. Például egy program egyszerûen meghívhatja a rnusers() C függvényt, amely megadja egy távoli gépen bejelentkezett felhasználók aktuális számát. A hívónak explicite nincs tudomása az RPC-rôl, ô csak egyszerûen egy eljárást hív meg, mintha pl. egy malloc()-ot hívna. Ebben a leírásban csak a C nyelvû RPC hívásokkal foglalkozunk, de tudnunk kell, hogy bármilyen programozási nyelvbôl lehet RPC-t hívni. Az RPC-t általában hálózatos kommunkációra használjuk, de annak sincs semmi akadálya, hogy két, azonos gépen futó processz közötti kommunikációra használjuk. 1.1.1.1. Terminológia Ebben a leírásban szerverekrôl, szolgáltatásokról, programokról, eljárásokról, kliensekrôl és verziókról esik szó. A szerver hálózati szolgáltatásokat kínál, amely gyûjteménye egy vagy több távoli programnak. A távoli program egy vagy több távoli eljárást implementál; az eljárásokat, azok paramétereit és visszatérési értékeit az adott program specifikációja dokumentálja. A hálózati kliensek kezdeményezik a szolgáltatások távoli eljárásainak meghívását. 1.1.1.2. Az RPC modell A távoli eljáráshívás modellje hasonlít a helyi eljárások hívásának modelljére. A helyi eljáráshívás esetén a hívó az eljáráshívás argumentumait egy ismert helyre teszi, ezután
84 Távoli eljáráshívás átadja a vezérlést az eljárásnak, majd annak lefutása után visszakapja a vezérlést. Ekkor a hívás eredményét a hívó elveszi a jól ismert helyrôl és tovább fut. A távoli eljáráshívás hasonlít erre abban, hogy a végrehajtás szála (thread of control) logikailag két folyamaton halad át. Egyik a hívó folyamat, a másik a szerver folyamat. Ez azt jelenti, hogy a hívó folyamat egy hívó üzenetet küld a szervernek, majd válaszra várakozik (block). A hívó üzenet tartalmazza egyebek mellett az eljárás paramétereit. A válasz tartalmazza egyebek mellett az eljárás eredményeit. Miután megérkezett a válaszüzenet, a kliens kiveszi abból az eredményt, majd továbbfut. A szerver oldalon egy alvó (várakozó) folyamat várja a hívó üzenetet. Amikor az megérkezik, kiveszi abból az eljárás paramétereit, elvégzi az eljárás feladatát, majd visszaküld egy válaszüzenetet és a következô hívásra várakozik. Vegyük észre, hogy ebben a modellben egyidôben csak egy folyamat aktív. Részletesebben lásd az 1. ábrát. kliens program szerver démon RPC hívás kliens program "B" gép Szolg. meghív. "A" gép Szolg. vegreh.. végrehajtás Vissz. ért. kliens program Válasz folytatódik 1. ábra Hálózati kommunikáció távoli eljáráshívással Az ábra szerint az RPC-ben rejtve maradnak a hálózat szállítási rétegének funkciói.
2.1. Verziók és számok 2. SPECIÁLIS HÁLÓZATI ISMERETEK Távoli eljáráshívás 85 Minden RPC eljárást egyértelmûen definiál egy program szám és egy eljárás szám. A program szám specifikálja összetartozó távoli eljárások egy csoportját. Ezen belül minden eljárásnak különbözô eljárás száma van. Minden programnak van egy verziószáma, így amikor kisebb változtatást eszközlünk egy távoli szolgáltatáson (például újabb eljárásokat valósítunk meg), nem kell egy újabb program számot adnunk, elég csak a verziószámot változtatnunk. Ha például azt szeretnénk, hogy egy eljárás megadja a távoli felhasználók számát, meg kell keresnünk a megfelelô programot, verzió és eljárásszámot a kézikönyvben, hasonlóan a memória allokátor nevéhez, ha pl. memóriát akarunk allokálni. 2.2. Portmap A portmap az egyetlen olyan hálózati szolgáltatás, amelynek kell, hogy legyen dedikált és mindenki által ismert portja (dedicated and well-known). Egyéb hálózati szolgáltatásokhoz statikusan vagy dinamikusan hozzárendelhetünk port számokat, amennyiben ezeket a portokat regisztráljuk hostjuk portmap-jával. A portmap program a számítógép bootolásakor automatikusan elindul. A szerver inicializálásakor meghívja az adott gépen futó portmap-ot, hogy az bejegyezze a szerver program és verziószámát. A kliens úgy találja meg a szerver portját, hogy küld a szerver gépen futó portmap-nak egy hívóüzenetet. Ha regisztrálva van az adott program a portmap-nál, akkor a portmap visszaküld egy RPC válaszüzenetet a kliensnek, amelyben megadja a szerver port számát. Ezután már a kliens közvetlenül küldhet üzenetet a szerver ilymódon megtudott portjára. Az általunk használt fejlesztôi környezet beépíti a portmap-ot a szerver oldali programba. 2.3. Transzport és szemantika Az RPC független a transzport protokolltól. Ez azt jelenti, hogy az RPC nem foglalkozik azzal, hogy miként adódik át egy üzenet egyik folyamattól a másiknak. A protokoll csak az üzenetek specifikációjával és interpretációjával foglalkozik. Fontos azt is látni, hogy az RPC nem foglalkozik semmilyen megbízhatósági követelmény kielégítésével, vagy azzal, hogy az alkalmazásnak tudnia kellene az RPC alatti szállítási rétegrôl. Amennyiben tudja, hogy egy megbízható réteg felett fut (pl. TCP/IP), akkor a legtöbb feladatát már ellátták. Amenyiben egy megbízhatatlan szállítási réteg (pl. UDP/IP) felett fut, akkor saját magának kell implementálnia a retranszmissziót és time-out-ot, mivel az RPC réteg nem ad semmiféle ilyen szolgáltatást.
86 Távoli eljáráshívás 2.3.1. A transzport megválasztása Az általunk használt Sun RPC-t támogatja az UDP/IP és a TCP/IP szállítasi réteg is. (Az elôzôekben megismert TLI Transport Layer Interface csak egy OSI kompatibilis felületet ad az UDP/IP és a TCP/IP rétegeknek, eltakarva azokat.) A szállítási réteg megválasztása függ az alkalmazás igényeitôl. A továbbiakban egyszerûsége és megbízhatósága miatt a TCP/IP-t fogjuk használni. 2.4. Külsô adatreprezentáció (external Data Representation, XDR) Az RPC feltételezi az XDR létezését, amely egy szabványa gépfüggetlen adatleírásnak és kódolásnak. Az XDR hasznos a különféle architektúrák közötti adatátvitelben és lehetôvé teszi olyan különféle gépek közötti adatcserét, mint pl. Sun workstation-ok, VAX, IBM-PC és Cray. Az RPC képes tetszôleges adatstruktúrák kezelésére, függetlenül a különféle gépek byte sorrendjére vagy adatstruktúra elrendezésére. Erre úgy van lehetôség, hogy az adatokat mielôtt a hálózatra küldenénk átalakítjuk XDR formátumra. Ezt a folyamatot szerializálásnak (serialization), a hálózatról vett adatok visszaállítását pedig deszerializálásnak (deserialization) hívjuk. 2.5. rpcinfo Az rpcinfo parancs használatával információt kérhetünk a portmap-nál bejegyzett programokról. Ez a parancs felvilágosítást ad az adott host-on bejegyzett RPC szolgáltatásokról, azok port számairól és az általuk használt szállítási rétegrôl. Bôvebb informació az rpcinfo(8c) manual page-ben található, a UNIX rendszereken. 2.6. A program számok kiosztása A kiosztást - 0x20000000 csoportokban - a következô táblázat tartalmazza. 0x00000000-0x1fffffff 0x20000000-0x3fffffff 0x40000000-0x5fffffff 0x60000000-0x7fffffff 0x80000000-0x9fffffff 0xa0000000-0xbfffffff 0xc0000000-0xdfffffff 0xe0000000-0xffffffff Sun által definiálva A felhasználó által definiálva Átmeneti Fenntartott Fenntartott Fenntartott Fenntartott Fenntartott
Távoli eljáráshívás 87 3. A FELADATMEGOLDÁSHOZ SZÜKSÉGES SPECIÁLIS ISMERETEK 3.1. rpcgen programozói leírás Az RPC használatának részletei nehézkesek és sok munkát igényelnek. Még nehezebb megírni az XDR konverziós rutinokat, melyek az eljáráshívások argumentumait és az eljárások eredményeit hálózatos formából belsô formába és fordítva konvertálják. Szerencsére létezik egy rcpgen(1) nevû program, amely hozzásegíti a programozókat a direkt és egyszerûbb RPC programozáshoz. Az rpcgen elvégzi a piszkos munka legnagyobb részét, ezáltal lehetôvé téve, hogy a programozó a program érdemi részének debuggolásával foglalkozhassék. Az rpcgen egy fordítóprogram (compiler). A távoli program interface definícóját egy RPC nyelvnek nevezett nyelven kell megadni, amely hasonlít a C nyelvhez. Az rpcgen outputja néhány C nyelvû forráslista, amely az RPC funkciókat fogja megvalósítani. Ez tartalmazza a kliens oldali RPC vázat (skeleton routines), a szerver oldali vázat, az XDR filter rutinokat a paraméterekre és visszatérési értekekre, egy közös definíciókat tartalmazó header (.h) file-t és opcionálisan olyan dispatch táblázatokat, amely alapján a szerver ellenôrizheti, hogy a kliensektôl érkezô kéréseknek van-e végrehajtási jogosultsága az adott szerveren. A kliens oldali interface váz valósítja meg az RPC-vel a kapcsolatot és rejti el hatékonyan az alsóbb hálózati rétegeket. A szerver oldali váz feladata hasonlóan az alsóbb rétegek elrejtése azon eljárások elôl, amelyeket a kliens hív. Az rpcgen output file-jait a C fordítóval normálisan lehet lefordítani és linkelni. A fejlesztônek ezután mindössze annyi a feladata, hogy megírja a szerver oldali eljárásokat (tetszôleges programnyelven, amely figyelembe veszi a rendszer rendszerhívási konvencióit) és ezeket összelinkelje az rpcgen által gyártott szerver oldali vázzal. Ilymódon egy, a szerver oldalon futtatandó, bináris programot kap. Ahhoz, hogy ezt a távoli programot használhassuk, a programozónak meg kell írnia egy közönséges main()- t tartalmazó programot, amely meghívja a kliens váz helyi eljárásait. Ezt a forrást kell összelinkelni az rpcgen által gyártott kliens oldali vázzal, ahhoz, hogy egy végrehajtható programot kapjunk. 3.1.1. Lokális eljárások távolivá tétele Tegyük fel, hogy van egy alkalmazásunk, amely már fut lokálisan, és most szeretnénk távolivá tenni. Ezt a konverziót egy olyan egyszerû programon mutatjuk be, amely egy üzenetet ír a képernyôre. Ez a program a prtmsg.c. Általában szükséges annak ismerete, hogy egy eljárásnak milyen típusú paraméterei és visszatérési értékei vannak. Ebben az esetben a printmessage() függvény egy
88 Távoli eljáráshívás stringet vesz el, és integert ad vissza. Ennek ismeretében megírhatjuk az RPC protokollunk specifikációját, amelyet le fogunk az rpcgen-nel fordíttatni. Ezt a specifikációt az msg.x file tartalmazza. A távoli eljárások távoli programok részei, így nem tettünk mást, mint egy teljes távoli programot deklaráltunk, amely egyetlen PRINTMESSAGE eljárást tartalmaz. Ezt az eljárást mint a távoli program 1-es verzióját deklaráltuk. Vegyük észre, hogy mindent nagybetûkkel írtunk. Erre nincs szükség, pusztán hasznos konvenció. Vegyük még azt is észre, hogy az argumentum típusa string és nem char *. Ez azért van, mert a char * a C-ben nem egyértelmû. A programozók ezen általában egy 0-végû karaktersorozatot értenek, de lehetne pl. egy darab karakter, vagy egy karaktertömbre mutató pointer is. Az RPC-ben a 0-végû karaktersorozatot egyértelmûen string-nek hívják. Már csak két dolgot kell megcsinálnunk. Az egyik maga a távoli eljárás. A következôk tartalmazzák a távoli eljárás definícióját, amely implementálja az elôzôekben deklarált PRINTMESSAGE-t. A programot az msg_proc.c tartalmazza. Ebben vegyük észre, hogy a printmessage_1() távoli eljárás három dologban különbözik a helyi printmessage() eljárástól: 1. Egy stringre mutató pointert vesz át egy string helyett. Ez minden távoli eljárásra igaz, hogy pointert vesz át argumentumára. Ha nincs argumentum, void-ot kell megadnunk. 2. Egy integerre mutató pointert ad vissza, egy integer helyett. Ez szintén jellemzô a távoli eljárásokra, hogy visszatérési értékeikre mutató pointereket adnak vissza. Ezért kell a visszatérési értéket mint static-ot deklarálni. Ha nincs visszatérési érték, akkor void-ot kell megadni. 3. A távoli eljárásnak a nevéhez egy _1 -es postfix kapcsolódik. Általában minden távoli eljárás, amelyet az rpcgen hív, a következô konvenció szerint kapja nevét: az eljárásdefiníciót átalakítja kisbetûkre, tesz utána egy aláhúzás ("_") karaktert és a verziószámot. Ezután már csak az marad, hogy deklaráljuk a kliens oldali fôprogramot, amely meghívja a távoli eljárást. Ez a program a rprtmsg.c. Érdemes még néhány dolgot megjegyezni: 1. Elôször megalkotjuk a clnt_create() RPC könyvtári függvénnyel a kliens handle-t. (azonosítót). Ezeket adjuk át a váz-rutinoknak, amelyek ténylegesen meghívják a távoli eljárást. 2. A clnt_create() utolsó paramétere "tcp", a szállítási szolgáltatás, amely felett az alkalmazást futtatni kívánjuk. (Ez lehetett volna udp is.)
Távoli eljáráshívás 89 3. A távoli printmessage_1() eljárást ugyanúgy kell hívni, mint ahogy azt az msg_proc.c -ben deklaráltuk, persze megadva utolsó argumentumként a client handle-t. 4. A távoli eljárás kétféleképpen mondhat csôdöt: vagy maga az RPC nem mûködik, vagy a távoli eljárásban lép fel hiba. A *result-ban van az eredmény. A Makefile írja le, hogyan kell összefordítanunk az egyes részeket. Az rpcgen a következôket csinálja az msg.x file lefordításakor: 1. Készít egy msg.h file-t, amely tartalmazza a #define-okat a MESSAGEPROG-hoz, MESSAGEVERS-hez és PRINTMESSAGE-hoz. 2. Készít kliens oldali váz rutinokat a msg_clnt.c file-ban. Ha DOS alatt dolgozunk, akkor 8 karakterre csonkol!!! Ezt a Makefile-ban figyelembe kell venni.) Esetünkben csak egy ilyen eljáráshoz, a printmessage()-hez. Ha az input file neve hihi.x, akkor az rpcgen által generált váz-program neve hihi_clnt.c. 3. Generálja az msg_svc.c szerver programot, amely meghívja a printmessage_1() eljárást. Az elnevezés hasonló az elôzöekhez: a hihi.x inputból az rpcgen készít egy szerver oldali hihi_svc.c nevû filet, amely tartalmazza a szerver oldali vázat. 3.2. Programlista melléklet 3.2.1. 1. példaprogram: távoli gépre egy üzenet kinyomtatása ************************************************************************ * Remote message printing in RPC * It contains: * prtmsg.c : local message printing (demo only) * msg.x : RPC language source * msg_proc.c : RPC Server routines * rprtmsg.c : RPC Client routines, it has main() * Makefile : How to assemble parts... ************************************************************************ * prtmsg.c : print a message on screen, local version #include <stdio.h> main (argc, argv) int argc; char *argv[]; char *message; if (argc!= 2)
90 Távoli eljáráshívás fprintf(sdterr, "usage: %s <message>\n", argv[0]); exit(1); message = argv[1]; if (!printmessage(message)) fprintf(stderr, "%s:couldn't print your message\n", argv[0]); exit(1); printf("message delivered!\n"); exit(0); * Print a message to screen. * Return a boolean indicating whether the message was actually printed. printmessage(msg) char *msg; fprintf(stdout, "%s\n", msg); return(1); ************************************************************************ * msg.x : RPC language source. Compile with rpcgen. * Remote message printing protocol program MESSAGEPROG Program name version MESSAGEVERS Version name int PRINTMESSAGE(string) = 1; 1st function declared = 1; Version number = 0x20000099; User defined Program number ************************************************************************* * msg_proc.c : implementation of the remote procedure "printmessage" * server side #include <stdio.h> #include <rpc/rpc.h> always needed #include "msg.h" msg.h generated by rpcgen * remote version of printmessage int *printmessage_1(msg) please note "_1"! char **msg; static int result; must be static! fprintf(stdout, "%s\n", *msg); result = 1; return (&result); return pointer ************************************************************************* * rprtmsg.c : remote version of prtmsg.c * client side #include <stdio.h> #include <rpc/rpc.h> #include "msg.h" main (argc, argv)
Távoli eljáráshívás 91 int char argc; *argv[]; CLIENT int char char *cl; *result; *server; *message; if (argc!= 3) fprintf(stderr, "usage: %s host message\n", argv[0]); exit(1); server = argv[1]; message = argv[2]; * Create client "handle" used for calling MESSAGEPROG on the server * designated on the command line. We tell the RPC package * to use the "tcp" protocol when contacting the server. cl = clnt_create(server, MESSAGEPROG, MESSAGEVERS, "tcp"); if (cl == NULL) * couldn't establish connection with server, * print error message and die. clnt_pcreateerror(server); exit(1); * Otherwise, call the remote procedure "printmessage" on the * server result = printmessage_1(&message, cl); * Remote procedure printmessage_1() is called exactly the * same way as it is declared in msg_proc.c, except * for the inserted handle as the second argument. if (result == NULL) * An error occurred while calling the server. * Print error message and die. clnt_perror(cl, server); exit(1); * OK, we successfully called the remote procedure. if (*result == 0) * Server was unable to print our message. * Print error message and die. fprintf(stderr, "%s: %s couldn't print your message\n", argv[0], server); exit(1); * The message got printed on the server printf("message delivered to %s\n", server); exit(0); ************************************************************************* # Makefile # Microsoft C 6.00 Compiler makefile. Run 'nmake' in its directory.
92 Távoli eljáráshívás # CC=cl BIN = rprtmsg.exe msg_serv.exe GEN = msg_cln.c msg_svc.c msg.h all: $(BIN) # Create server (msg_svc.c is generated by running 'rpcgen msg.x') msg_serv.exe: msg.h msg_proc.c msg_svc.c $(CC) -o $@ msg_proc.c msg_svc.c /link ssunrpc snetlib spc ssocket sdomain # Create client (msg_clnt.c is generated by running 'rpcgen msg.x') rprtmsg.exe: msg.h rprtmsg.c msg_clnt.c $(CC) -o $@ rprtmsg.c msg_clnt.c /link ssunrpc snetlib spc ssocket sdomain # We also need msg.h, created by 'rpcgen' msg.h: msg.x rpcgen msg.x #Clean up nicely clean: l:\tools\rm -los $(GEN) *.obj $(BIN) *********************************************************************** 3.1.2. 2. példaprogram: távoli gép idejének lekérdezése ********************************************************************* * Remote date service in RPC (gets date and time from another machine) * It contains: * date.x : RPC Language source. Compile with 'rpcgen date.x' * date_pro.c : RPC Server routines * rdate.c : RPC Client routines * Makefile : How to assemble parts in MS C 6.00... ********************************************************************* ******************************************************************** * date.x - Specification of remote date and time service. * Define 2 procedures: * bin_date_1() returns the binary time and date (no arguments). * str_date_1() takes a binary time and returns a human-readable string. program DATE_PROG version DATE_VERS long BIN_DATE(void) = 1; procedure number = 1 string STR_DATE(long) = 2; procedure number = 2 = 1; version number = 1 = 0x31234567; program number = 0x31234567, allowed * This RPC Language file defines the DATE_PROG server and client * function. * Data type declarations are similar to the C language * Server program (date_pro.c) has to implement functions: * long *bin_date_1() and char **str_date_1(bintime) * long *bintime; * This following code is an RPC Language example of a type definition * * typedef string str<128>; * the string itself * * * struct stringarray
Távoli eljáráshívás 93 * str ss<128>; * ; * * The following code is a constant definition * * const MAXSORTSIZE = 64; ******************************************************************** * date_pro.c - remote procedures; called by server stub. #include <rpc/rpc.h> standard RPC include file #include "date.h" this file is generated by rpcgen * Return the binary date and time. long * bin_date_1() static long timeval; must be static long time(); Unix function timeval = time((long *) 0); return(&timeval); * Convert a binary time and return a human readable string. char ** str_date_1(bintime) long *bintime; static char *ptr; must be static char *ctime(); Unix function ptr = ctime(bintime); convert to local time return(&ptr); return the address of pointer ******************************************************************** * rdate.c - client program for remote date service. #include <stdio.h> #include <rpc/rpc.h> standard RPC include file #include "date.h" this file is generated by rpcgen main(argc, argv) int argc; char *argv[]; CLIENT *cl; RPC handle char *server; long *lresult; return value from bin_date_1() char **sresult; return value from str_date_1() if (argc!= 2) fprintf(stderr, "usage: %s hostname\n", argv[0]); exit(1); server = argv[1]; * Create the client "handle."
94 Távoli eljáráshívás if ( (cl = clnt_create(server, DATE_PROG, DATE_VERS, "udp")) == NULL) * Couldn't establish connection with server. clnt_pcreateerror(server); exit(2); * First call the remote procedure "bin_date_1". * declared in rdate.x if ( (lresult = bin_date_1(null, cl)) == NULL) clnt_perror(cl, server); exit(3); printf("time on host %s = %ld\n", server, *lresult); * Now call the remote procedure "str_date_1". if ( (sresult = str_date_1(lresult, cl)) == NULL) clnt_perror(cl, server); exit(4); printf("time on host %s = %s", server, *sresult); clnt_destroy(cl); done with the handle exit(0); ******************************************************************** # Microsoft C 6.00 Makkefile Run 'nmake' in its directory # CC=cl BIN = date_svc.exe rdate.exe GEN = date_cln.c date_svc.c date.h all: $(BIN) #Create server date_svc.exe: date.h date_pro.c date_svc.c $(CC) -o $@ date_pro.c date_svc.c /link ssunrpc snetlib spc ssocket sdomain #Create client rdate.exe: date.h rdate.c date_cln.c $(CC) -o $@ rdate.c date_cln.c /link ssunrpc snetlib spc ssocket sdomain #Generate date.h date.h: date.x rpcgen date.x #Clean up nicely clean: c:\mks\rm -los $(GEN) *.obj $(BIN) ********************************************************************