Elosztott rendszerek programozása Jávában


Tartalomjegyzék:
Távoli módszerhívás
Csonkok és csontvázak
Paraméterátadás
Kliens és szerver egymásra találása
Hibakezelés
Példaprogram
Interfészek
Kiszolgáló implementációja
Ügyfél
Fordítás és futtatás
Összegzés

A számítógép-hálózatok rohamos terjedésével a hálózattal összekapcsolt számítógépekbõl álló rendszerek egyre népszerûbbek, hiszen lehetõvé teszik a rendszer erõforrásainak szélesebb körû megosztását (resource sharing), a rendszer megbízhatóságának (reliability) illetve teljesítményének növelését, valamint a felhasználók egymás közti üzenetváltásait.. Elosztottnak (distributed) az irodalom olyan - ún. lazán csatolt, hálózattal összekötött - rendszert nevez, amelynek felhasználója elõl a lehetõ legnagyobb mértékben rejtve marad az, hogy a rendszer nem egyetlen központi számítógépbõl áll.

 Programozók szempontjából elosztott egy rendszer, ha a lehetõ legkevésbé kell a hálózati protokollok, kommunikáció programozásával törõdnie. A Jáva nyelvnek már a megjelenésénél is hangoztatott, kiemelt tulajdonsága az "elosztottság" volt. Sajnos ez a az elsõ nyilvános verzióban, a JDK 1.0-ban még inkább csak ígéretnek bizonyult. A korai könyvtárak támogatták ugyan a szállítási szintû hálózati kommunikáció egyszerû programozását - mint ahogy azt pl. a múlt havi cikkben is láthattuk -, azonban az alkalmazások erre épülõ protokollját már külön kellett beprogramozni.

Az 1997. januárjában megjelent új Jáva alap-környezet, a JDK (Java Development Kit) 1.1-es változata az RMI könyvtár megvalósításával jelentõs újításokat hozott ezen a téren.

Távoli módszerhívás

Az elosztott rendszerek programozására korábban kialakult egyik módszer az ún. távoli eljáráshívás (RPC, Remote Procedure Call). Itt a programozó a fejlesztése során egy processzoros rendszerben gondolkozik, ahol egyszerû eljáráshívással valósítja meg a program részei között a kommunikációt. A fejlesztés egy késõbbi lépésében szétválaszthatja a hívó és hívott eljárásokat, más-más számítógépre telepítve azokat. A felhasznált rendszerkönyvtárak biztosítják, hogy a hálózaton keresztül is az eljáráshívás, a paraméterek átadása és átvétele automatikusan, a programozó elõl "rejtetten" történjen meg.

Objektumorientált programokban nem globális eljárásokat, hanem objektumok módszereit hívjuk, ezért távoli eljáráshívás helyett itt távoli módszerhívásról (RMI, Remote Method Invocation) beszélhetünk.

Csonkok és csontvázak

Egy processzoros rendszerben a kliens objektumok a szerver objektumok osztályában definiált nyilvános módszereket hívhatják meg. A paraméterek átadásáról a virtuális gép gondoskodik.

 

Kliens és szerver

Távoli módszerhívásnál a kliens és a szerver objektum különbözõ számítógépre kerül. Így a kliens közvetlenül nem tudja a szerver valamelyik módszerét meghívni. A hívási láncban a kliens oldalán egy objektum csonk (stub) kerül, amely rendelkezik a szerver objektum minden nyilvános módszerével. A csonk feladata, hogy a módszerek paramétereit "becsomagolja" és a hálózaton keresztül eljuttassa a szerver számítógépre. Itt a távoli hívást egy ún. szerver csontváz (skeleton) várja, amely fogadja a kérést, elveszi a paramétereket és meghívja az eredeti szerver objektum megfelelõ módszerét. Amikor a módszer lefutott, a lefutás tényérõl, valamint az esetleges visszaadott értékekrõl, bekövetkezett kivételekrõl a várakozó kliens ugyancsak a csontváz-csonk kapcsolat segítségével értesül.

 

Csont és csontváz

Az RMI-ben az a szép, hogy a csonk és a csontváz automatikusan keletkezik, nem kell külön programoznunk. Ehhez mindössze egy interfészben - amely a java.rmi.Remote interfészt bõvíti - definiálnunk kell a szerver objektum távolról hívható módszereit, majd ezeket a módszereket egy osztállyal - a szerverrel - implementálnunk kell. A lefordított class állományból az rmic program generálja mind a csonk, mind a csontváz program lefordított kódját.

Paraméterátadás

A módszerek legtöbbje paraméterekkel is rendelkezik, sõt esetleg visszatérési értéket is hordozhat. Helyi módszerhívásoknál a paraméterátadás nem jelenet gondot, pontosabban a programozási nyelv rejtett mechanizmusai, a Jáva virtuális gép gondoskodik róla. Sokkal problematikusabb a paraméterátadás megvalósítása a hálózaton keresztül. Például gondoljuk meg, hogy a Jáva objektumaira mindig referencia hivatkozik, ezt a referenciát - ha úgy tetszik, memóriacímet - viszont nem lehet egyszerûen átküldeni a hálózaton, mert ugyanaz a cím a másik számítógépen nem fog ugyanarra az objektumra hivatkozni.

A távoli eljáráshívások (RPC) legnagyobb problémája az összetett adatszerkezetek hálózaton keresztüli átvitele. Leggyakrabban az átvihetõ adatszerkezetek szigorú korlátozásával, a programtól független specifikációjával segítik a paraméterátadást.

A Jáva egyszerûbb, egységesebb szerkezete, a referenciák következetes, kizárólagos használata, no meg a Jáva virtuális gép tulajdonságai miatt az RMI-nél a paraméterátadás, legalábbis a programozó számára nem jelent semmiféle korlátozásokat. Távoli módszereink tetszõleges paraméterekkel rendelkezhetnek, a paraméterátadás alapvetõen az objektum másolatának átadásával történik. A csonk az átadandó objektum állapotát (tagváltozóinak értékét) átalakítja a hálózaton átvihetõ, ún. soros reprezentációvá, amelyet a másik oldal, a csontváz visszaalakít, létrehozva a szerver oldalon egy lokális objektumot, majd erre utaló hivatkozást ad tovább a szerver módszer törzsének.

Amennyiben paraméterként beépített Jáva adattípusokat, vagy a JDK könyvtárak osztályait használjuk, semmi extra teendõnk nincsen, a sorosítás-visszaalakítás (serialization-deserialization) automatikusan megtörténik. Ha paraméterként saját osztályaink példányait is használni akarjuk, akkor ezeknek az osztályoknak implementálniuk kell a JDK 1.1-ben újonnan megjelent java.io.Serializable interfészt. Szerencsére ez az interfész csak opcionálisan megvalósítandó módszereket tartalmaz, legtöbbször elegendõ az osztály fejében leírnunk, hogy

 

class myClass implements java.io.Serializable

Megjegyzés: a sorosítás támogatása a JDK 1.1-nek ugyancsak új tulajdonsága, amely a távoli módszerhíváson túl például objektumok állományokban tárolására, egyfajta perzisztencia megvalósítására is használható. Sajnos a cikk terjedelmi korlátai miatt ezzel a könyvtárral jelenleg részletesebben nem foglalkozhatok.

A paraméterátadásnál érdekes eset, amikor egy olyan objektumra hivatkozást akarunk átadni, amely maga is távolról hívható módszerekkel rendelkezik. Ilyenkor a kiszolgáló oldalra az objektum másolata helyett egy csonk kerül csak át, amely segítségével a kiszolgáló - immár "ügyfélként" viselkedve -  visszahívhatja ezt a távoli objektumot.

Kliens és szerver egymásra találása

A távoli módszerhívásnál némi bonyodalmat jelent a kliens és a szerver egymásra találása. Egy gépes esetben ez statikusan, a fordítóprogram vagy Jáva esetén az osztály betöltõ (class loader) segítségével megtörténik, elosztott rendszerben viszont futás közben, egy speciális "tudakozó" szolgáltatás segítségével jön létre. A kiszolgáló oldalán található, távolról hívható objektumokat létrejöttükkor be kell jelenteni - regisztráltatni - kell tudakozónak. Az ügyfél a távoli számítógépre és az azon található kiszolgáló objektum nevére hivatkozva a tudakozótól kapja meg azt a csonkot, amely a távoli módszerhívást majd lebonyolítja. A csonk módszereit meghívva már közvetlenül a kiválasztott kiszolgáló objektummal kommunikálhatunk.

Hibakezelés

Sajnos a hálózati kommunikáció közben a hálózati protokollok minden ügyessége ellenére sok olyan hiba történhet, amely lokális módszerhívások esetén nem fordulhat elõ. Ezen hibákat a programból illene kezelni. A Jáva kivételkezelõ mechanizmusa kézenfekvõ módszert nyújt a váratlan hibák kezelésére. Minden távolról meghívható módszer elõállíthat java.rmi.RemoteException kivételt, amelyet a hívó vagy elkap és kezel, vagy majd a hívó környezetre foglalkozik vele, mindenesetre észrevétlen nem maradhat.

Példaprogram

A távoli módszerhívást illusztráló példaprogram bonyolultabb, mint a "Halló világ!" ügyfél-kiszolgáló környezetre adaptált változata. Természetesen az én példámnak sincs sokkal több "értelme", mégis bonyolultabb, aszinkron kommunikációt illusztrál.

A példa szerverünk egy telefonos ébresztõ szolgáltatásra hasonlít. Az ügyfelek a kiszolgálónak bejelenthetik, hogy mikor - esetünkben hány milliszekundum múlva - kérnek ébresztést, a szerver megjegyzi az igényeket és a kívánt idõ eltelte után visszahívja az egyes klienseket. A kommunikáció attól aszinkron, hogy a kliens az igénye bejelentése után nem kell, hogy tényleg "aludjon", azaz várakozzon, hanem tetszõleges tevékenységet végezhet, ébresztéskor a kiszolgáló az ügyfél egy módszerét hívja majd vissza.

Interfészek

Elsõ lépésként a távoli módszereket leíró interfészeket kell definiálnunk. Figyelem, itt mindkét oldal távolról meghívható eljárásokat tartalmaz: a kiszolgáló az ébresztés kérést, az ügyfél a visszahívást. Tehát két interfészt definiálunk. A programunk összes objektumát egy alarm nevû pakkba fogjuk össze.

Az ébresztõ szolgáltatás távolról hívható módszerének leírása (AlarmServer.java):

Az ügyfél megadja a várakozás hosszán túl azt az objektumot, például saját magát, amely módszerét - a wakeup()-ot, ld. lentebb - kell majd visszahívni.

Az ébresztést kérõ objektum távolról hívható módszerének leírása (AlarmClient.java):

Látható, hogy mindkét interfész a java.rmi.Remote-ot bõvíti, minden módszer lenyomatában szerepel az esetleg elõálló java.rmi.RemoteException kivétel.

Kiszolgáló implementációja

Következõ lépésként valósítsuk meg a kiszolgálót (AlarmServerImpl.java). A feladatunk az objektumorientált tervezési módszertanban jól ismert - a politikai nyelvezetben kissé rosszul hangzó, 3/3-as ízû - ún. megfigyelõ-megfigyelt (observer-observable) tervezési minta egyszerû változata. A minta lényege, hogy a rendszerben fontos - megfigyelt - eseményekhez egy kiszolgáló objektumot rendelünk. Az egyes megfigyelõk regisztrálják magukat a kiszolgálónál, amely az esemény bekövetkeztekor azok mindegyikét értesíti egy megadott módszerük megadásánál.

Mellesleg a Jáva környezet támogatja ezt a tervezési mintát (java.util.Observable osztály és java.util.Observer interfész), de itt ezt a feladat egyszerûsége és terjedelmi korlátok miatt nem használom. A minta teljes implementációjánál a kiszolgáló legalább két módszert definiál, amely segítségével megfigyelõ objektumok regisztrálhatják magukat (addObserver), illetve visszaléphetnek (deleteObserver), a kiszolgáló dinamikus adatszerkezetben tárolja az éppen regisztrált megfigyelõket. Mivel a példánkban az egyes megfigyelõk, azaz az ébresztésre váró folyamatok csak egyetlen eseményre várnak, visszalépésre nincs szükség, az ébresztés után a szerver megfeledkezik róluk.

A kiszolgálót implementáló osztálynak egy speciális osztályból, a java.rmi.server.UnicastRemoteObject-bõl kell leszármaznia és természetesen meg kell valósítania a távoli hívások interfészét. A konstruktor érdekessége, hogy a szülõ osztály konstruktora miatt esetleg java.rmi.RemoteException kivételt okozhat. Az ébresztés kérést nagyon egyszerûen implementálom, minden kéréshez új ébresztõórát (AlarmClock) készítek. Az ébresztõóra megkapja az ébresztendõ objektum hivatkozását. A beállított idõ lejárta után erre az ébresztõórára a program nem fog hivatkozni többé, tehát a Jáva szemétgyûjtése automatikusan megszabadul tõle. Az ébresztõ kiszolgáló program elõször egy új, a távoli módszerhívások védelmére szolgáló biztonsági menedzsert telepít. Eztán létrehoz egy kiszolgáló példányt, majd ezt a tudakozónál regisztráltatja (Naming.rebind). Az összes hibát az egyszerûség kedvéért egy helyen kapjuk el és kiírjuk csupán. Itt következik az ébresztõóránk implementációja. Az objektum konstruktora megjegyzi az ébresztendõ objektumot és a várakozás idejét, majd létrehoz és elindít egy párhuzamos szálat. A párhuzamos szál tevékenysége is nagyon egyszerû, csak alszik a megadott ideig, majd visszahívja az ébresztendõ objektum wakeup módszerét. Igaz, hogy a távoli hívásnál hiba is elõállhat, ám itt nem nagyon tudunk vele mit kezdeni: feltehetõleg megszakadt a kapcsolat, egyszerûen úgy vesszük, mintha az ébresztés megtörtént volna. Tudom, hogy ez nem hangzik túl barátságosan, de ne felejtsük el, hogy a távoli módszerhívást megvalósító hálózati protokollok már mindent megtettek azért, hogy felvegyék a távoli objektummal a kapcsolatot. Ha nekik sem sikerült, nekem sincs jobb ötletem!

Ügyfél

Kiszolgálónk kipróbálására készítsünk egyszerû ügyfelet (AlarmApplet.java). Programkát írunk, azaz a kódot egy böngészõ letöltheti és futtathatja. Természetesen írhattunk volna önálló programot is, de a programka jobban illusztrálja a Jáva lehetõségeit: az ügyfél programunk egyszerû Web szerverrõl bárhova letölthetõ és futtatható. Sajnos jelenleg még csak nagyon kevés elterjedt böngészõ, pontosabban csak a Sun HotJava böngészõjének legutóbbi próba változata, illetve a JDK-val érkezõ "böngészõ-pótló", az appletviewer támogatja a JDK 1.1-et, de hamarosan mind a Netscape, mind a Microsoft is támogatni fogja. A programka kódjában csak egyetlen csavar van: mivel egy távoli módszert kell meghívnia ezért elõbb meg kell az érintett objektumot találnia. A Naming.lookup hívás szolgál erre, amelynek paramétereként megadjuk azt a Web kiszolgálót (getCodeBase().getHost()), ahonnan maga a programka letöltõdött, valamint az ébresztõ szolgáltatás nevét (AlarmServer). Arról se feledkezzünk el, hogy bár ez programka, de önmaga is távolról hívható módszert definiál, tehát saját magát is regisztráltatni kell, ez történik az exportObject hívással. Ezek után nincs más dolgunk, mint hogy egy tájékoztató üzenetet írjunk ki, pontosabban helyezzünk el egy egyedváltozóban, majd a paint kiírja. Majd meghívjuk az ébresztõ szolgáltatást 10 másodperces idõzítéssel. Az összes hibát itt is egyszerre kezeljük, kiírva a hibaüzenetet. A programka paint módszere simán kiírja az aktuális, tárolt szöveget a programka ablakába. Az ébresztéskor meghívott módszer sem túl bonyolult, egyszerûen kicseréli a kijelzett szöveget és kezdeményezi a képernyõ frissítését. A programkánkhoz tartozik egy HTML lap is (Alarm.html, sajnos itt nem próbálható ki, a kiszolgáló ugyanis nem fut), amely segítségével ez letölthetõ:

Fordítás és futtatás

Eddig 4 Jáva forrásállományt és egy HTML lapot írtunk. Mivel ezeket egy alarm nevû pakkba helyeztük, a forrásállományokat is tegyük egy alarm nevû könyvtárba.

Elsõ lépésként fordítsuk le Jáva forrásokat. Az itt közölt parancsállomány Windows 95 környezetben, helyesen telepített JDK 1.1-es környezettel mûködnek.

Az eredeti CLASSPATH változót elmentjük és az aktuális könyvtár (ezt alarm-nak hívjuk) szülõjét hozzácsapjuk az eredetihez. A sikeres fordítás után az eredményül kapott class állományokból a csonk és a csontváz létrehozása történik meg. Következõ lépésként - ha még nem futna - a tudakozót, majd az ébresztõ kiszolgálót indítjuk. Távoli géprõl kipróbálva a böngészõt kellene indítanunk a http://saját-szerverünk/valahol/alarm/Alarm.html URL-lel. Kipróbálhatjuk a helyi gépünkön is, mivel a Windows 95 is több párhuzamos tevékenységet képes egyidejûleg futtatni. Az appletviewer indítása elõtt visszaállítjuk az eredeti CLASSPATH változót, hogy a böngészõnk a szükséges osztályokat a "hálózaton" keresztül töltse le.

Összegzés

A példaprogramunk ugyan egyszerû, de ne feledjük el, hogy azért teljes ügyfél-kiszolgáló rendszert valósítottunk meg anélkül, hogy a hálózati kommunikációval törõdnünk kellett volna.

A megoldásunk egyetlen "hátránya" - már aki ezt annak tekinti -, hogy csak Jávában megírt objektumok képesek egymással kommunikálni. Azonban ez sem igaz sokáig, mostanában jelenik meg egy nagyon hasonló technológia Jáva szintû támogatása: az Object Management Group (OMG) által elfogadott, ipari szabványnak tekinthetõ Common Object Request Broker Architecture (CORBA) 2.0-s változatához illeszkedõ objektumokat írhatunk. Ezek aztán bármilyen CORBA kompatíbilis objektumot hívhatunk, sõt azok is hívhatnak minket, a többi objektum akár más nyelveken is íródhatott. Hamarosan ezt az RMI-nél sokkal flexibilisebb rendszert is támogatja a JDK, illetve a böngészõk.

 


Kiss István

  updated: 97/05/04, http://www.eunet.hu/infopen/cikkek/java/rmi.html 1