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.
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.
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.
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.
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
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.
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.
Az ébresztõ szolgáltatás távolról hívható módszerének leírása (AlarmServer.java):
package alarm; public interface AlarmServer extends java.rmi.Remote { public void alarm(AlarmClient client, long sleepInMillis) throws java.rmi.RemoteException; }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):
package alarm; public interface AlarmClient extends java.rmi.Remote { public void wakeup() throws java.rmi.RemoteException; }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.
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.
package alarm; import java.rmi.*; import java.rmi.server.UnicastRemoteObject;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.
public class AlarmServerImpl extends UnicastRemoteObject implements AlarmServer { private String name;A konstruktor érdekessége, hogy a szülõ osztály konstruktora miatt esetleg java.rmi.RemoteException kivételt okozhat.
AlarmServerImpl(String s) throws java.rmi.RemoteException { super(); name = s; }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.
public void alarm(AlarmClient client, long sleepInMillis) throws java.rmi.RemoteException { new AlarmClock(client, sleepInMillis); }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.
public static void main(String[] args) { System.setSecurityManager(new RMISecurityManager()); try { AlarmServerImpl server = new AlarmServerImpl("AlarmServer"); Naming.rebind("AlarmServer", server); } catch (Exception e) { System.out.println("AlarmServer: an exception occurred!"); e.printStackTrace(); } } }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.
class AlarmClock implements Runnable { AlarmClient client; long sleepInMillis; Thread clock; AlarmClock (AlarmClient client, long sleepInMillis) { this.client = client; this.sleepInMillis = sleepInMillis; clock = new Thread(this); clock.start(); }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!
public void run() { try { clock.sleep(sleepInMillis); } catch (InterruptedException e) {}; try { client.wakeup(); } catch (RemoteException e) {}; } }
package alarm; import java.awt.*; import java.applet.*; import java.rmi.*; import java.rmi.server.*; public class AlarmApplet extends Applet implements AlarmClient { String message; public void init() {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.
try { AlarmServer server = (AlarmServer)Naming.lookup("//" + getCodeBase().getHost() + "/AlarmServer"); UnicastRemoteObject.exportObject(this);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.
message = "I'm sleeping!"; server.alarm(this, 10000); } catch (Exception e) { message = "AlarmApplet exception: " + e.getMessage(); } }A programka paint módszere simán kiírja az aktuális, tárolt szöveget a programka ablakába.
public void paint(Graphics g) { g.drawString(message, 25, 50); }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.
public void wakeup() { message = "I'm awakened!"; repaint(); } }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õ:
<HTML> <HEAD> <TITLE>Testing Alarm Server</TITLE> </HEAD> <BODY> <H2 align=center>Testing RMI Alarm Server</H2> <CENTER> <APPLET CODEBASE=".." CODE="alarm.AlarmApplet" WIDTH=300 HEIGHT=100> </APPLET> </CENTER> </BODY> </HTML>
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.
set ORIGCLASSPATH=%CLASSPATH% set CLASSPATH=..;%CLASSPATH%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.
javac -d .. *.javaA 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.
rmic -d .. alarm.AlarmServerImpl alarm.AlarmAppletKövetkezõ lépésként - ha még nem futna - a tudakozót, majd az ébresztõ kiszolgálót indítjuk.
start rmiregistry start java alarm.AlarmServerImplTá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.
set CLASSPATH=%ORIGCLASSPATH% appletviewer Alarm.html
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.
updated: 97/05/04, http://www.eunet.hu/infopen/cikkek/java/rmi.html