Remote Method Invocation, stub, skeleton, proxy, garbage collection, remote procedure call, serialization, transient, rmiregistry, security manager
RMI - Remote Method Invocation este un mecanism care extinde funcţionalitatea RPC, permiţând invocarea unei metode a unui obiect care există într-un alt spaţiu de adresă, în contextul unei mașini virtuale diferite.
RMI implementează comunicaţia la distanţă doar între programe scrise în limbajul de programare Java.
Obiectivul RMI s-a concentrat pe a pune la dispoziţia programatorilor mijloacele de a dezvolta aplicaţii distribuite în mod transparent pentru programator, astfel încât acesta să apeleze metodele în acelaşi fel în care ar face-o pentru aplicaţii nedistribuite (folosind o sintaxă şi o structură semantică asemănătoare), încât folosirea de obiecte distribuite să fie similară cu utilizarea de obiecte locale, diferenţele fiind sintetizate mai jos:
Concept | Obiect local | Obiect “la distanţă” |
---|---|---|
definirea obiectului | se face într-o clasă Java | definirea comportamentului (exportat) al obiectului “la distanţă” se face printr-o interfaţă care trebuie să extindă interfaţa java.rmi.Remote |
implementarea obiectului | se face în clasa Java corespunzătoare | comportamentul obiectului accesat “la distanţă” este realizat printr-o clasă care implementează interfaţa |
crearea obiectului | instanţa unui obiect nou se face prin operatorul new | o instanţă nouă pentru un obiect aflat “la distanţă” se face folosind operatorul new pe maşina gazdă (clientul nu poate crea direct obiectul “la distanţă”) |
accesul obiectului | se realizează direct printr-o variabilă de tip referinţă | un obiect “la distanţă” este accesat printr-o referinţă care indică spre un delegat al implementării interfeţei |
referinţe | o referinţă indică direct spre obiectul din heap corespunzător | o referinţă “la distanţă” indică spre un obiect delegat (stub) alocat local în heap; obiectul delegat conţine informaţii ce permit conectarea la obiectul “la distanţă” unde este realizată implementarea metodelor |
referinţe active | un obiect este considerat “activ” dacă există cel puţin o referinţă către el | într-un mediu distribuit unde maşinile virtuale pot manifesta erori critice sau conexiunea poate fi pierdută, o referinţă este activă pentru un obiect la distanţă dacă accesarea se face într-o anumită perioadă de timp (de închiriere); dacă pentru un obiect s-a renunţat (în mod explicit) la toate referinţele sau toate referinţele au expirat, obiectul “la distanţă” este disponibil pentru colectarea memoriei (eng. garbage collection) distribuită |
finalizarea | metoda finalize() (în caz că este definită) se apelează înainte ca memoria aferentă obiectului să fie dezalocată (prin garbage collector) | dacă obiectul “la distanţă” implementează interfaţa Unreferrenced , metoda definită de această interfaţă este apelată când referinţele la obiect sunt distruse |
colectarea memoriei disponibile | un obiect spre care nu mai există referinţe (locale) devine candidat pentru mecanismul de colectare a memoriei disponibile (pentru dezalocarea memoriei aferente) | există modul de colectare a memoriei distribuit care lucrează cu modulul garbage collector local, apelându-se atunci când nu există referinţe realizate “la distanţă” şi toate referinţele locale pentru obiectul accesat “la distanţă” au fost distruse |
excepţii | un program trebuie să trateze toate excepţiile (runtime sau alt tip) | pentru asigurarea robusteţii aplicaţiilor distribuite, programele trebuie să trateze excepţiile RemoteException care ar putea fi generate |
Proiectarea mecanismului RMI a vizat implementarea unui model de obiecte distribuite în Java care să se integreze în limbajul de programare şi totodată să fie compatibil cu modelul de obiecte locale.
Aplicaţiile RMI sunt compuse, de cele mai multe ori, din două programe separate, server şi client.
Un server creează nişte obiecte la distanţă, face accesibile referinţele spre obiecte şi aşteaptă clienţii să invoce metodele pe care le-au definit.
Un client obţine o referinţă spre unul sau mai multe astfel de obiecte la distanţă care se găsesc pe server şi apoi invocă metodele pe ele.
RMI oferă un mecanism prin care serverul şi clientul comunică şi transferă informaţia în ambele sensuri. O astfel de aplicaţie este denumită cel mai frecvent aplicaţie (cu obiecte) distribuite.
Aplicaţiile distribuite trebuie să realizeze următoarele operaţii: localizarea obiectelor la distanţă, comunicarea cu obiectele la distanţă, încărcarea definiţiilor de clase pentru obiectele care sunt obţinute.
Se observă că serverul foloseşte serviciul de nume (rmiregistry) pentru a înregistra un obiect pe care apoi clientul îl găseşte invocând acelaşi serviciu. Ulterior, clientul apelează o metodă a obiectului “la distanţă”. Definiţiile de clase sunt obţinute pentru obiect de pe un server web (existent), în ambele direcţii (de la server spre client şi de la client spre server).
Unul dintre avantajele principale puse la dispoziţie de mecanismul RMI este obţinerea definiţiei clasei unui obiect care nu este definit în maşina virtuală Java a clientului. Toate atributele şi metodele unui obiect, disponibile până acum într-o singură maşină virtuală, pot fi transmise către o alta, posibil la distanţă. Transmiterea obiectului se face prin clasa sa, astfel încât comportamentul nu se modifică atunci când acesta este transmis către altă maşină virtuală. Astfel, noi atribute şi comportamente sunt introduse într-o maşină virtuală aflată la distanţă, îmbogăţind astfel comportamentul aplicaţiei.
Ca orice aplicaţie Java, o aplicaţie distribuită folosind mecanismul RMI conţine interfeţe (pentru a declara metode) şi clase (pentru a implementa metodele definite în interfeţe şi, posibil, pentru a implementa metode noi). Unele implementări se pot găsi doar pe unele maşini virtuale. Obiectele invocate între maşini virtuale diferite sunt denumite obiecte “la distanţă” (eng. remote objects).
Un obiect “la distanţă” implementează o interfaţă (vizibilă “la distanţă”) având următoarele caracteristici:
java.rmi.Remote
;java.rmi.RemoteException
, în plus faţă de alte excepţii;Obiectele “la distanţă” sunt tratate diferit (faţă de obiectele locale) prin mecanismul RMI atunci când sunt transmise de la o maşină virtuală la alta.
În loc să se facă o copiere a implementării obiectului în maşina virtuală (client), mecanismul RMI transmite pentru obiectul la distanţă un ciot (eng. stub) care are rolul de delegat (eng. proxy), reprezentant local al obiectului şi reprezintă referinţa pentru acesta pe maşina virtuală (client). Astfel, se va invoca metoda pe obiectul ciot local ce este responsabil pentru execuţia metodei invocate în obiectul la distanţă.
Un ciot pentru un obiect la distanţă implementează acelaşi set de interfeţe pe care le implementează şi obiectul la distanţă, proprietate ce permite ciotului să poată fi convertit la oricare din interfeţele obiectului la distanţă. Totuşi, doar metodele definite în interfaţă sunt disponibile spre a fi invocate de client.
Tehnologia RMI foloseşte mecanismul de serializare a obiectelor din Java pentru a transmite obiecte prin valoare între maşini virtuale diferite. Pentru ca un obiect să fie considerat serializabil, clasa sa trebuie să implementeze interfaţa java.io.Serializable
.
Există două tipuri de clase care pot fi folosite cu mecanismul RMI:
Remote
ale căror instanţe pot fi folosite la distanţă, obiectele instanţă ale acestei clase putând fi accesate în două moduri:Serializable
ale căror instanţe pot fi copiate între spaţii de adrese.Dacă un obiect serializabil este dat ca parametru (sau valoare întoarsă) pentru un apel de metodă la distanţă, valoarea obiectului va fi copiată dintr-un spaţiu de adrese în altul. De asemenea, dacă un obiect la distanţă este transmis ca parametru (sau valoare întoarsă), delegatul acestuia va fi copiat dintr-un spaţiu de adrese în altul.
Majoritatea claselor standard sunt serializabile, deci o subclasă ale oricărora dintre acestea este de asemenea serializabilă. Orice informaţie dintr-o clasă serializabilă ar trebui să fie serializabilă.
Folosirea unui obiect serializabil în apelul unei metode la distanţă este foarte simplă. Se specifică obiectul dat ca parametru sau valoare întoarsă, tipul acestuia trebuind să fie o clasă serializabilă, fiind necesar ca atât clientul cât şi serverul să aibă acces la definiţia clasei serializabile care este utilizat, descărcarea definiţiilor claselor serializabile de pe o maşină virtuală pe alta trebuind să fie specificată prin politici de securitate.
Etapele dezvoltării unei aplicaţii distribuite folosind mecanismul RMI constau în:
javac
atât pentru interfeţele la distanţă, implementarea lor, alte clase pe server, cât şi pentru clasele client;Aplicaţiile distribuite bazate pe mecanismul RMI sunt de regulă aplicaţii bazate pe comportamente căci sarcinile sunt încărcate dinamic de catre client fără a exista cunoştinţe în prealabil despre clasele care implementează sarcinile respective.
În cele ce urmează, implementarea unei aplicaţii distribuite folosind mecanismul RMI va fi ilustrată pe cazul particular al unui serviciu de rezervare a locurilor.
Rezervările pot fi realizate numai în cadrul unui program de funcționare (diferit pentru fiecare dată calendaristică în parte), serviciul fiind limitat la un anumit număr de locuri. Aceste informaţii sunt disponibile în fişierul timetable.txt
din directorul input
de pe sever, fiind încărcate la momentul lansării în execuţie a acestuia. În fişier se indică numărul de locuri disponibile, precum şi programul de funcţionare pentru fiecare zi în parte, respectând formatul ZZ LL AAAA O1 M1 O2 M2
(unde O1:M1
reprezintă momentul de timp de la care se doreşte să se facă rezervarea, iar O2:M2
momentul de timp până la care aceasta este valabilă).
Aplicaţia va avea arhitectura client-server şi are definite semnăturile pentru metodele pe care va trebui să le implementaţi.
Pe server (ReservationImplementation.java
) se vor reţine rezervările clienţilor fără a fi necesară asigurarea persistenţei (informaţiile sunt disponibile cât timp serverul rulează, pierzându-se ulterior). Tot aici vor fi implementate metodele din interfaţa pe care o cunoaşte şi clientul, fiind apelate în acest context.
Fiecare client (Client.java
) poate realiza o rezervare specificând o perioadă de timp preferată (orice durată) şi un număr de locuri. Dacă perioada de timp se încadrează în orarul specificat şi în intervalul respectiv există un număr de locuri libere corespunzător cererii făcute, atunci rezervarea este realizată şi locurile respective marcate ca ocupate. Este reţinut totodată şi identificatorul clientului care a realizat rezervarea, astfel încât tot acesta să aibă posibilitatea de a o anula, dacă situaţia o impune. Informaţiile cu privire la rezervările pe care clientul doreşte să le realizeze sunt preluate de la tastatură, respectând acelaşi format ca şi în fişier (dată calendaristică, intervalul orar pentru rezervarea respectivă, numărul de locuri solicitat), fără a fi necesară dezvoltarea unei interfeţe grafice.
Protocolul care permite ca serverul să fie interogat cu cereri de către client care să fie rulate apoi de server iar rezultatul să fie întors la client este realizat prin intermediul unei interfeţe care descrie funcţionalitatea care poate fi accesată la distanţă.
import java.rmi.Remote; import java.rmi.RemoteException; import java.util.ArrayList; public interface Reservation extends Remote { public ArrayList<Interval> getTimetable() throws RemoteException; public int getAvailableSeats(Interval interval) throws RemoteException; public boolean makeReservation(int clientId, Interval interval, int numberOfSeats) throws RemoteException; public boolean cancelReservation(int clientId, Interval interval) throws RemoteException; }
Prin extinderea interfeţei java.rmi.Remote
se specifică faptul că metodele declarate de interfaţa Reservation
vor putea fi apelate din altă maşină virtuală Java. Orice obiect care implementează interfaţa Reservation
va putea fi considerat un obiect “la distanţă”.
Metodele definite în cadrul acestei interfeţe vor putea fi apelate din cadrul altei maşini virtuale (“la distanţă”), prin urmare este obligatoriu să genereze excepţii de tip java.rmi.RemoteException
. O astfel de excepţie este aruncată de sistemul RMI dintr-un apel de metodă la distanţă pentru indicarea situaţiei în care a intervenit o eroare de comunicare sau de protocol. Codul sursă care apelează metode “la distanţă” trebuie să trateze o astfel de excepţie (prin prinderea ei sau transmiterea ei mai departe).
Se observă că metodele getAvailableSeats()
, makeReservation()
şi cancelReservation()
au şi un parametru de tip Interval
, întoarcând un rezultat de tip int
/boolean
. Pentru că Interval
nu reprezintă un tip de bază, el trebuie definit tot acum.
import java.io.Serializable; import java.util.GregorianCalendar; public class Interval implements Serializable { private static final long serialVersionUID = 2014L; GregorianCalendar startingTime; GregorianCalendar endingTime; public Interval() { startingTime = new GregorianCalendar(); endingTime = new GregorianCalendar(); } public Interval(GregorianCalendar startingTime, GregorianCalendar endingTime) { this.startingTime = startingTime; this.endingTime = endingTime; } public void setStartingTime(GregorianCalendar startingTime) { this.startingTime = startingTime; } public GregorianCalendar getStartingTime() { return startingTime; } public void setEndingTime(GregorianCalendar endingTime) { this.endingTime = endingTime; } public GregorianCalendar getEndingTime() { return endingTime; } }
Câmpul serialVersionUID
e folosit în procesul de deserializare al obiectelor încât dacă atributul obţinut nu corespunde cu cel specificat în definiţia clasei, este aruncată o excepţie de tipul InvalidClassException
.
Obiectele de tip Interval
vor fi transmise între client şi server astfel încât ele trebuie să fie serializabile (asigurând astfel consistenţa informaţiilor reţinute în obiect indiferent de formatul de reprezentare din maşinile virtuale). De aceea, clasa Interval
implementează interfaţa Serializable
(din pachetul java.io
) şi pune la dispoziţie un constructor de copiere şi metode de tip setter/getter .
Compilarea claselor se face în mod obişnuit:
$ javac Reservation.java Interval.java
Întrucât cele două clase sunt necesare atât pe client cât si pe server, acestea trebuie împachetate într-o arhivă Java (de tip jar) adăugată în classpath atât la compilare cât şi la rulare.
$ jar cvf reservation.jar Reservation.class Interval.class
Serverul trebuie să implementeze interfaţa “la distanţă” vizibilă în reţea astfel încât comportamentul obiectelor care vor fi accesate de client să fie definit (obiectele “la distanţă” sunt obţinute prin instanţierea clasei Server). Pe server pot fi definite şi alte metode (în afara celei impuse de interfaţă) care vor putea fi accesate local, între care constructorul clasei şi metoda main()
.
Argumentele sau valorile întoarse din metodele “la distanţă” (definite în interfaţă) pot fi de orice tip, inclusiv obiecte locale, obiecte “la distanţă” sau chiar tipuri primitive de date. Astfel, orice entitate având orice tip poate fi transmisă către sau de la o metodă “la distanţă” cât timp aceasta este o instanţă a unui tip care este fie primitiv, fie obiect “la distanţă” sau obiect local serializabil (aşa cum s-a precizat şi mai sus). Alte tipuri de obiecte, cum ar fi fire de execuţie sau descriptori de fişier, având sens doar în spaţiul de adrese în care rulează, nu pot să fie utilizaţi ca argumente sau valori întoarse în metode “la distanţă”. Totuşi, majoritatea claselor din pachetele java.lang
şi java.util
implementează interfaţa java.io.Serializable
.
Obiectele “la distanţă” sunt transmise prin referinţă (un ciot care este delegatul la nivelul clientului şi care implementează setul complet de interfeţe accesibile prin reţea specificate de obiect).
Sunt vizibile doar metodele definite în interfaţa “la distanţă” pentru obiectul respectiv. Metodele definite pentru obiect care nu sunt definite în interfaţă nu vor putea fi accesate la distanţă.
Orice modificare realizată asupra stării obiectului prin apelurile la distanţă sunt reflectate în obiectul original (de pe server).
Obiectele locale sunt transmise prin valoare (este realizată o copie folosind mecanismul de serializare). Sunt copiate toate câmpurile care nu au precizat atributul static
sau transient
. Orice modificare realizată în starea obiectului sunt reflectate doar în copie, dar nu şi în instanţa originală. Similar, schimbările asupra stării obiectului în instanţa originală nu vor fi reflectate în copie.
import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.UnicastRemoteObject; public class Server { public static void main (String[] args) { if (System.getSecurityManager() == null) System.setSecurityManager(new SecurityManager()); try { String serviceName = "ReservationService"; Reservation service = new ReservationImplementation(); Reservation proxy = (Reservation)UnicastRemoteObject.exportObject(service, 0); Registry registry = LocateRegistry.getRegistry(); registry.rebind(serviceName, proxy); } catch (Exception exception) { System.out.println("An exception has occurred: "+exception.getMessage()); if (Constants.DEBUG) exception.printStackTrace(); } } }
import java.rmi.RemoteException; import java.util.ArrayList; public class ReservationImplementation implements Reservation { public ReservationServer() { // ... } public ArrayList<Interval> getTimetable() throws RemoteException { // ... } public int getAvailableSeats(Interval interval) throws RemoteException { // ... } public boolean makeReservation(int customerId, Interval interval, int numberOfSeats) throws RemoteException { // ... } public boolean cancelReservation(int customerId, Interval interval) throws RemoteException { // ... } }
Metoda main()
este cea mai complexă, fiind folosită pentru a porni serverul, realizând totodată şi iniţializările necesare pentru a pregăti serverul să accepte conexiuni dinspre clienţi. Metoda nu poate fi apelată din altă maşină virtuală, pentru că nu extinde interfaţa Remote
. Fiind statică, ea este asociată cu clasa şi nu cu obiectul.
ReservationServer
şi este exportată către serviciul de nume RMI folosindu-se metoda UnicastRemoteObject.exportObject()
astfel încât obiectul să poată fi apelat de către clienţi de la distanţă. La întoarcerea din execuţia acestei metode, obiectul poate procesa apeluri “la distanţă”. Cel de-al doilea argument al metodei exportObject()
reprezintă un port TCP utilizat pentru ascultarea apelurilor metodelor la distanţă de către client. Valoarea 0, folosită în exemplul de faţă specifică utilizarea unui port anonim, ales de RMI sau de sistemul de operare.Reservation
, deci tipul interfeţei şi nu al clasei (pentru că ciotul implementează doar interfaţa “la distanţă”).Mecanismul RMI pune la dispoziţie un tip particular de obiect “la distanţă” și anume serviciul de nume (RMI registry), pentru găsirea de referinţe către alte obiecte de acest tip prin specificarea unui nume. Un astfel de sistem este în mod obişnuit folosit doar pentru găsirea primului obiect solicitat de client, putând ajuta la găsirea altor obiecte.
Interfaţa java.rmi.registry.Registry
conţine specificaţiile pentru înregistrarea şi găsirea de obiecte “la distanţă” în serviciul de nume. Clasa java.rmi.registry.LocateRegistry
pune la dispoziţie metode statice pentru identificarea unei referinţe la distanţă într-o reţea anume (specificată prin adresă şi port). Metodele creează referinţa către obiectul “la distanţă” conţinând adresa de reţea specificată fără a se realiza comunicare la distanţă. De asemenea, clasa java.rmi.registry.LocateRegistry
pune la dispoziţie metode statice pentru crearea unui serviciu de nume nou în maşina virtuală Java.
După ce obiectul “la distanţă” a fost înregistrat de sistemul de nume, clienţii de pe orice maşină îl pot căuta după nume, îi pot obţine referinţa şi pot apela metodele “la distanţă” de pe el. Serverele de pe o maşină pot partaja acelaşi serviciu de nume sau fiecare aplicaţie (de tip server) poate să îşi creeze şi să utilizeze propriul său sistem de nume.
Metoda rebind()
reprezintă un apel la distanţă către serviciul de nume RMI şi poate avea ca rezultat aruncarea unei excepţii de tip RemoteException
.
Metoda LocateRegistry.getRegistry()
este apelată fără parametri, obţinându-se o referinţă la serviciul de nume de pe maşina locală şi portul 1099, definit implicit. Dacă serviciul de nume a fost creat pe un alt port, se foloseşte o metodă supraîncărcată care primeşte un parametru de tip int care specifică portul.
Când se face apelul la distanţă către serviciul de nume, se obţine un obiect de tip ciot (în loc de copia obiectului) pentru că obiectele care implementează interfeţe de tip Remote
rămân în cadrul maşinii virtuale Java în care au fost create. Astfel, atunci când un client realizează o căutare în serviciul de nume al serverului, se întoarce o copie a obiectului de tip ciot, obiectele “la distanţă” fiind transmise deci prin referinţă.
Din motive de securitate, o aplicaţie poate (re)construi sau distruge legătura dintre o referinţă a unui obiect la distanţă şi serviciul de nume care rulează pe aceeaşi maşină, prevenind situaţia în care o altă maşină ar înlătura sau suprascrie intrările într-un serviciu de nume.
Operaţia de căutare (lookup()
) poate fi realizată de pe orice maşină, locală sau “la distanţă”.
Excepţiile ce pot fi aruncate de metoda main()
sunt de tip RemoteException
, generate fie de metoda UnicastRemoteObject.exportObject()
sau de apelul rebind()
. Se poate realiza recuperarea din eroare (în caz că o excepţie a fost aruncată) prin reapelarea metodei care a generat-o sau prin folosirea unui alt server.
După realizarea acestor operaţii, metoda main()
se încheie. Nu este necesară definirea unui fir de execuţie care să ţină serverul în starea de rulare, pentru că atât timp cât există o referinţă către un obiect de tip ReservationServer
într-o maşină virtuală (locală sau la distanţă), el nu va fi distrus de garbage collector. Mecanismul RMI menţine activ procesul asociat obiectului ReservationServer
, pentru că există o legătură către el în serviciul de nume, accesibilă la distanţă. Obiectul va fi distrus atunci când legătura dintre el şi serviciul de nume va fi eliberată şi nu vor mai exista clienţi la distanţă care să aibă referinţe către el.
Compilarea claselor se face în mod obişnuit:
$ javac –cp libs/reservation.jar Server.java
Lansarea în execuţie nu poate fi realizată decât după:
> start rmiregistry [port] –J-Djava.rmi.server.useCodebaseOnly=false
$ rmiregistry [port] –J-Djava.rmi.server.useCodebaseOnly=false &
policy[.txt]
, codul sursă obţinând permisiunile de care are nevoie pentru a putea rula: grant codebase "file://<cale_absoluta>/ReservationServer/-" { permission java.security.AllPermission; };
Toate permisiunile sunt acordate claselor din calea locală a aplicaţiei pentru că acesta are un grad de încredere ridicat, dar nu se acordă drepturi pentru codul sursă descărcat din alte maşini virtuale.
Lansarea în execuţie a serverului se face prin comanda:
> java -cp .;libs/reservation.jar -Djava.rmi.server.codebase=file://<cale_absoluta>/ReservationServer/libs/reservation.jar -Djava.rmi.server.hostname=localhost -Djava.security.policy=configuration/policy Server
> java -cp .:libs/reservation.jar -Djava.rmi.server.codebase=file://<cale_absoluta>/ReservationServer/libs/reservation.jar -Djava.rmi.server.hostname=localhost -Djava.security.policy=configuration/policy Server
Proprietatea java.rmi.server.codebase
specifică locaţia de unde pot fi descărcate definiţii pentru clasele provenind de la server. Dacă în locul unei arhive se specifică o ierarhie de directoare, aceasta trebuie încheiată prin caracterul ’/’
.
Proprietatea java.rmi.server.hostname
specifică numele maşinii sau adresa care urmează a fi completată în obiectele de tip ciot care vor fi exportate. Aceeaşi valoare va fi folosită de clienţi când vor încerca să apeleze metodele la distanţă. Implicit, implementarea RMI foloseşte adresa IP indicată de java.net.InetAddress.getLocalHost()
. Totuşi, câteodată o astfel de adresă nu este întotdeauna potrivită pentru toţi clienţii şi un nume (al maşinii) este mai adecvat. Pentru ca numele sa fie vizibil de toţi clienţii, trebuie configurată proprietatea java.rmi.server.hostname
.
Proprietatea java.rmi.policy
specifică politica care conţine permisiunile ce vor fi acordate.
Codul care apelează metodele unui obiect tip Reservation
trebuie să obţină o referinţă către acesta.
Ulterior, va creea un obiect de tip Interval
(eventual din parametrii specificaţi în linie de comandă) pe care îl va transmite ca argument metodelor makeReservation()
sau cancelReservation()
.
LocateRegistry.getRegistry()
pentru a obţine referinţa la sistemul de nume RMI care rulează pe maşina server, parametrul cu care este apelat reprezentând numele maşinii pe care rulează obiectul de tip Reservation
. În mod implicit, se consideră că sistemul de nume RMI ascultă pe portul 1099. În plus, poate fi folosită o metodă supraîncărcată care să specifice şi portul, prin specificarea unui parametru de tip int
. Apoi este utilizată metoda lookup()
pentru a căuta obiectul la distanţă prin nume în sistemul de nume de pe maşina server.Interval
(eventual din parametrii specificaţi în linia de comandă) şi se vor apela metodele la distanţă, afişându-se rezultatul.import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class Client { public static void main(String[] args) { if (System.getSecurityManager() == null) System.setSecurityManager(new SecurityManager()); try { String serviceName = "ReservationService"; Registry registry = LocateRegistry.getRegistry(args[0]); Reservation reservation = (Reservation)registry.lookup(serviceName); int customerId = 0; Interval interval = new Interval(); int numberOfSeats = 0; if (reservation.makeReservation(customerId,interval,numberOfSeats)) { System.out.println("The reservation has been made successfully"); } else { System.out.println("The reservation has not been made successfully"); } } catch (Exception exception) { System.out.println("An exception has occurred: "+exception.getMessage()); if (Constants.DEBUG) exception.printStackTrace(); } } }
Compilarea claselor se face în mod obişnuit:
$ javac –cp libs/reservation.jar Client.java
Lansarea în execuţie nu poate fi realizată decât după:
ReservationServer
;policy[.txt]
, codul sursă obţinând permisiunile de care are nevoie pentru a putea rula: grant codebase "file://<cale_absoluta>/ReservationClient/-" { permission java.security.AllPermission; };
Lansarea în execuţie a serverului se face prin comanda:
> java -cp .;libs/reservation.jar -Djava.rmi.server.codebase=file://<cale_absoluta>/ReservationClient/libs/reservation.jar -Djava.security.policy=configuration/policy Client <rmiregistry_IP_address>
> java -cp .:libs/reservation.jar -Djava.rmi.server.codebase=file://<cale_absoluta>/ReservationClient/libs/reservation.jar -Djava.security.policy=configuration/policy Client <rmiregistry_IP_address>
Se observă că prin proprietatea java.rmi.server.codebase
s-a precizat locaţia unde clientul îşi defineşte clasele, iar prin proprietatea java.security.policy
s-a specificat fişierul care conţine permisiunile acordate pentru diferite coduri sursă.
Proprietatea codebase
din fişierul ce conţine politica de securitate (atât pentru server cât şi pentru client) trebuie să ofere permisiuni maxime pentru toate clasele aflate în structura de directoare a serverului, respectiv a clientului.
Configuraţiile de rulare implică specificarea proprietăţilor Java java.rmi.server.codebase
(ce indică spre arhiva .jar ce conţine clasele care trebuie încărcate – în cazul serverului, respectiv directorul în care clientul îşi defineşte clasele – în cazul clientului), java.rmi.server.hostname
pentru server şi java.security.policy
atât pentru server cât şi pentru client.
Modificarea configuraţiei de rulare în NetBeans 8.0.1
Modificarea configuraţiei de rulare în Eclipse 4.4.1 (Luna SR1)
Nu este neapărat necesar ca proprietățile să fie specificate în cadrul configurațiilor de rulare, acestea pot fi precizate și în codul sursă (înainte de a se implementa politica de securitate):
System.setProperty("java.rmi.server.codebase", "file://..."); System.setProperty("java.security.policy", "configuration/policy");
aipi2014-lab04-remoteinterfaces.zip
1. [0 puncte] Să se pornească registrul de nume rmiregistry
prin apelarea scripturilor startRmiRegistry [.bat|.sh]
.
În cazul când calea către directorul bin al SDK-ului Java nu este precizată în variabila de mediu PATH
, acest script nu va fi executat cu succes.
Precizarea căii către directorul bin
al SDK-ului Java în variabila de mediu PATH
se realizează astfel:
export PATH=${PATH}:/usr/lib/jvm/default-java/bin
SET PATH=%PATH%;C:\Program Files\Java\jdk1.8.0_25\bin
rmiregistry
rulează înainte de a porni serverul.aipi2014@ubuntu:~$ ps -a
rmiregistry
corespunde versiunii cu care dezvoltaţi aplicaţiile, altfel conexiunea la serviciul de nume va eşua.
2. [0 puncte] a) Să se recreeze arhiva .jar conținând definițiile de clase, prin compilarea surselor din aipi2014-lab04-remoteinterfaces.zip
și împachetarea lor.
aipi2014@ubuntu:~$ javac ro/pub/cs/aipi/lab04/reservation/*.java aipi2014@ubuntu:~$ jar cvf reservation.jar ro/pub/cs/aipi/lab04/reservation/*.class
Să se suprascrie fișierul reservation.jar
obținut în proiectele corespunzătoare serverului (ReservationServer
) și clientului (ReservationClient
), în directorul lib/
. Se asigură astfel că sursele încărcate au fost compilate folosind aceeași versiune de Java ca și serverul / clientul, care vor folosi definițiile de clase respective.
b) Să se modifice căile absolute din fişierul policy
corespunzător serverului, respectiv din configuraţiile de rulare (Eclipse / Netbeans) pentru rularea serverului și a clientului.
3. [10 puncte] Să se încarce orarul de funcţionare al restaurantului în constructorul clasei ReservationServer
cu datele conţinute de fişierul timetable.txt
.
Se va parsa conţinutul fişierului timetable.txt
care pe prima linie conţine capacitatea (numărul de locuri) încărcată în variabila numberOfSeats
iar pe următoarele linii programul pentru fiecare zi calendaristică exprimat sub forma ZZ LL AAAA O1 M1 O2 M2
, acesta fiind încărcat în obiectul timetable
.
GregorianCalendar
, lunile sunt numerotate începând cu valoarea 0.
4. [40 puncte] Să se implementeze metoda getAvailableSeats()
a clasei ReservationServer
.
Metoda verifică faptul că intervalul respectiv se regăseşte în programul de funcţionare şi în caz afirmativ se determină câte locuri dintre cele libere pot fi rezervate.
Exemplu. Clientul cu identificatorul 6 doreşte rezervarea a 4 locuri în intervalul orar 1800 → 1930. Considerând situaţia rezervărilor în perioada respectivă:
numărul de locuri disponibile în cadrul intervalului orar se determină ca minim al numărului de locuri din intervalele determinate ca intersecţie dintre intervalul respectiv şi intervalele în care au fost făcute celelalte rezervări.
Pentru exemplul dat, intervalele pentru care se va calcula numărul de locuri vor fi: (1800,1815), (1815,1830), (1830,1915), (1915,1945), (1945,2000).
Descrierea în pseudocod a algoritmului poate fi:
getAvailableSeats(interval) { result = numberOfSeats; // check if interval is in timetable for-all(currentInterval ∈ timetable) if (interval.getStartingTime() < currentInterval.getStartingTime() || interval.getEndingTime() > currentInterval.getEndingTime()) return -1; // determine intersection points with existing reservations GregorianCalendar[] intersectionPoints; for-each(currentReservation ∈ reservations) { Interval intersection = getIntervalIntersection(interval,currentReservation.getInterval()); if (intersection != null) { if (!intersectionPoints.contains(intersection.getStartingTime()) intersectionPoints.add(intersection.getStartingTime()); if (!intersectionPoints.contains(intersection.getEndingTime()) intersectionPoints.add(intersection.getEndingTime()); } } if (intersectionPoints ≠ ∅) { // sort intersection points sort(intersectionPoints); // get number of seats available for each subinterval for (i=0; i<intersectionPoints.size()-1; i++) { // create subinterval currentInterval = new Interval(intersectionPoints[i],intersectionPoints[i+1]); currentNumberOfSeats = numberOfSeats; // determine number of seats available in the subinterval by intersection with // occupied seats in reservations for (j=0; j<reservations.size(); j++) if (getIntervalIntersection(currentInterval,reservations[j].getInterval())!=null) currentNumberOfSeats -= reservations[j].getNumberOfSeats(); // number of seats available in the interval is given by the minimum number // of seats available in all subintervals if (result > currentNumberOfSeats) result = currentNumberOfSeats; } } return result; }
În implementare, puteţi folosi metoda
getIntervalIntersection(Interval interval1, Interval interval2);
care întoarce intervalul obţinut prin intersecţia altor două intervale.
GregorianCalendar
defineşte metodele before()
şi after()
spre a verifica dacă o dată calendaristică se află înainte sau după altă dată calendaristică.
5. [10 puncte] Să se implementeze metoda makeReservation()
a clasei ReservationServer
.
Metoda va verifica dacă rezervarea poate fi făcută, apelând metoda getAvailableSeats()
şi testând că numărul de locuri pentru care se doreşte rezervarea în intervalul precizat este disponibil.
6. [10 puncte] Să se implementeze metoda cancelReservation()
a clasei ReservationServer
.
Metoda va verifica faptul că există o rezervare a clientului respectiv pentru intervalul orar precizat.
7. [10 puncte] Să se apeleze metodele makeReservation()
şi cancelReservation()
din clasa ReservationClient
. Informaţiile utilizate ca parametrii ale acestor metode vor fi citite de la tastatură sau pot fi definite drept constante.
8. [10 puncte] Să se verifice comportamentul la distanţă al aplicaţiei, apelând metodele implementate de alți colegi.
Pentru ca serverul implementat de colegii voştri să fie vizibil către clientul cu care doriţi să îl accesaţi, componentele trebuie să aibă adrese IP publice sau să se găsească în aceeaşi reţea de calculatoare în cadrul căreia să aibă adrese IP private din acelaşi spaţiu (mască de reţea / gateway).
9. [20 puncte] Să se implementeze metoda ArrayList<ReservationInformation> getReservation(int clientId)
ca fiind apelabilă la distanţă, aceasta întorcând lista cu intervalele în care clientul respectiv are făcute rezervări, precum şi numărul de locuri rezervate în fiecare dintre cazuri.
ReservationInformation
, care conține informații despre rezervările utilizatorului, va trebui să fie serializabilă.