<

11. Programowanie rozproszone:

identyfikacja zasobów (JNDI) i protokół RMI-IIOP


W tym punkcie poruszymy ważny temat lokalizacji zasobów w środowiskach rozproszonych. Bardzo dobrze będzie widoczne jego znaczenie przy omawianiu protokołu RMI-IIOP. Ten zaś jest podstawą interakcji w serwerach J2EE.


1. Serwisy nazw i katalogów

Koncepcja serwisów nazw i katalogów (naming and directory services) polega ogólnie na tym, aby w sposób zunifikowany, niezależny od fizycznych właściwości danego zasobu i bez konieczności znajomości jego fizycznej lokalizacji móc uzyskać do niego dostęp. Zasoby mogą mieć dowolną naturę - np. mogą to być obiekty, drukarki, komputery itp.

Serwisy nazw wiążą zasoby z identyfikatorami (nazwami).
Np. system plikowy jest swoistym serwisem nazw, który pozwala nam po nazwach odwoływać się do plików.
Przykładem innego serwisu nazw jest DNS.

Zbiór par powiązań pomiędzy nazwami a zasobami oznaczanym przez te nazwy nazywa się kontekstem


Konteksty są zazwyczaj zorganizowane w hierarchiczne struktury, zaczynające sie od tzw. inicjalnego kontekstu (initial context).  Ten kontekst - właściwy dla danego serwisu - określa konwencje nazewnicze i zawiera określone podkonteksty.
W każdym kontekście mogą być zawarte podkonteksty.

Przykład: system plikowy. Inicjalny kontekst /, podkontekst /usr itd.

W ramach kontekstu możliwe są określone operacje, np. związania nazwy z zasobem, uzyskanie dostępu do zasobu po jego nazwie.

Serwisy słownikowe poszerzają te koncepcje o możliwości nadawania zasobom nie tylko nazw, ale i atrybutów i odpowiednie operacje (m.in. wyszukiwania).

2. JNDI - koncepcja i architektura

W Javie dostępny jest Java Naming and Directory Interface (JNDI) - specjalne API pozwalające uzyskiwać dostęp do serwisów nazw i katalogów.

Operowanie na zasobach za pomocą serwisów nazw i katalogów wymaga określonych kroków:
  1. Uruchomienie odpowiedniego serwera nazw, dostarczającego wymaganego serwisu (zazwyczaj)
  2. Uzyskanie inicjalnego kontekstu dla tego serwisu (Context ctx = new InitialContext(...))
  3. Ew. dodawanie lub wyszukiwanie podkontekstów (przechodzenie do podkontekstów - ctx.lookup(podkontekst))
  4. Odnajdywanie zasobu po nazwie (ctx.lookup("NazwaZasobu")) lub wiązanie nazwy z zasobem w danym kontekście (ctx.bind("NazwaZasobu", refDoZasobu) lub ctx.rebind("NazwaZasobu", refDoZasobu)).
Architektura JNDI jest pomyślana na zasadzie "plugins". Odpowiednie łączniki do określonych serwisów są instalowane dynamicznie w postaci tzw. service provider implementation (SPI). Implementują one inicjalne konteksty odpowiednie dla danego rodzaju serwisu.

r

źródło: JNDI Tutorial.

W standardowej dystrybucji Javy znajdują się następujace gotowe implementacje SPI:
Inne mogą być uzyskane z Internetu (m.in. ze strony JNDI, np. filesystem service provider, umożliwiający stosowanie JNDI do systemu plikowego, albo Windows Registry Service Provider - do dzialania na rejestrze Windows (wersje komercyjne). Odpowiednie serwery (np. orb czy serwer jms) mogą dostarczać swoich inicjallnych kontekstów. Serwery aplikacji - łączą w sobie różne serwisy i dają własne inicjalne konteksty dla różnych przypadków.

3. JNDI - praktyczne przykłady

Aby uzyskać inicjalny kontekst stosujemy konstruktor InitialContext().
Uzyskanie inicjalnego kontekstu wymaga dostarczenia parametrów m.in.
Możemy też (a czasem musimy) podać parametry związane z bezpieczeństwem (np. protokół) czy preferowanym językiem serwisu.

Wszystkie te parametry określane są jako właściwości (properties). Przy czym mogą to być:

Właściwości mają swoje nazwy np.:
fabryka inicjalnego kontekstu - java.naming.factory.initial
URL dostawcy serwisu - java.naming.provider.url

Zobaczmy to na przykładach.
Niech dostawcą serwisu będzie filesystem service provider (JNDI w użyciu do systemu plikowego - trzeba ściągnąć pakiet ze strony JNDI).

Zastosowanie właściwości środowiskowych
Hashtable env = new Hashtable(11);
env.put(Context.INITIAL_CONTEXT_FACTORY,
        "com.sun.jndi.fscontext.RefFSContextFactory");
Context initCtx = new InitialContext(env);

// Metoda lookup pozwala na odnalezienie zasobu:

Object obj = initCtx.lookup("e:/temp");

Zastosowanie właściwości aplikacji
Plik jndi.properties
java.naming.factory.initial=com.sun.jndi.fscontext.RefFSContextFactory
Fragment programu:
Context initCtx = new InitialContext();  // konstruktor bezparametrowy

// Metoda lookup pozwala na odnalezienie zasobu:

Object obj = initCtx.lookup("e:/temp");

Zastosowanie właściwości systemowych

Program jest taki sam jak poprzednio (używamy konstruktora bezparametrowego), ale JVM wywołujemy z odpowiednimi argumentami:

java -Djava.naming.factory.initial=com.sun.jndi.fscontext.RefFSContextFactory Prg
(Prg jest przykładową nazwą klasy programu).

Te trzy źródła identyfikacji SPI (właściwości środowiskowe,  systemowe i właściwości aplikacji  z jndi.properties) są używane i ze sobą łączone (w wymienionej kolejności).



Oczywiście, w realnych aplikacjach rozproszonych serwery nazw zajmują się kontekstami, a klienci - uzyskują dostęp do zasobów po nazwach.

Generalna procedura jest następująca:

Za chwilę zobaczymy to w praktycznym działaniu.

Jednak serwery aplikacji dostarczają niejako gotowych  kontekstów, do ktorych możemy mieć dostęp z inicjalnego kontekstu (o który specjalnie nie musimy się martwić), a wiązanie nazw z zasobami (bind) odłożone jest do fazy wdrożeniowej (deskryptory wdrożenia). Za to wiązanie odpowiedzialne są kontenery.
Dzięki temu uzyskuje się izolację kodów od konkretnych warunków i konfiguracji wdrożeniowych.


Np. znany nam już z "Metod Programowania" sposób programowania połączeń bazodanowych w środowisku Tomcata (dający pooling połączeń):
  DataSource dataSource;  // źrodło danych

  public void init() throws ServletException {
    try {
      Context init = new InitialContext();
      Context contx = (Context) init.lookup("java:comp/env");
      dataSource = (DataSource) contx.lookup("jdbc/ksidb");
     } catch (NamingException exc) {
        throw new ServletException(
          "Nie mogę uzyskać źródła java:comp/env/jdbc/ksidb", exc);
     }
  }
W deskryptorze kontekstu  (faza wdrożeniowa, plik nazywany zwykle plik context.xml) podajemy np:

  <Resource name="jdbc/ksidb" auth="Container"
            type="javax.sql.DataSource"
            description="Baza danych ksiazek"
            driverClassName="com.mysql.jdbc.Driver"
            url="jdbc:mysql://localhost/ksidb"
            username="admin"
            password="admin"
            maxActive="20" />


Zwróćmy uwagę, że ogólnie serwery J2EE dostarczają standardowego kontekstu java:comp/env, z którego możemy mieć dostęp do różnych podkontekstów. Np. serwer Sun Application Serwer dostarcza następujących podkontekstów:

Rodzaj zasobu
Typ zasobu
Podkontekst JNDI 
JDBC
javax.sql.DataSource
java:comp/env/jdbc
JMS
javax.jms.TopicConnectionFactory
javax.jms.QueueConnectionFactory
java:comp/env/jms
JavaMail
javax.mail.Session
java:comp/env/mail
URL
java.net.URL
java:comp/env/url
Connector
javax.resource.cci.ConnectionFactory
java:comp/env/eis
JAXR Resource Adapter
javax.xml.registry.ConnectionFactory
java:comp/env/eis/JAXR
Źródło: J2EE Tutorial

W środowisku serwerów aplikacji poprzez JNDI mamy też dostęp do prostych właściwości, obiektów EJB, konektorów itp.
Np.  do prostych właściwości (wartości typów opakowujących typy pierwotne oraz typu String), specyfikowanych w deskryptorach wdrożenia jako env-entry jest bardzo prosty:

Fragment deskryptora wdrożenia:
    <env-entry>
      <description>Maksymalna temperatura</description>
      <env-entry-name>maxTemp</env-entry-name>
      <env-entry-type>java.lang.Integer</env-entry-type>
      <env-entry-value>37</env-entry-value>
    </env-entry>
i kod programu:

InitialContext iniCtx = new InitialContext();
Context envCtx = (Context) iniCtx.lookup("java:comp/env");
Integer maxExemptions = (Integer) envCtx.lookup("maxTemp");
To samo dotyczy dostępu do Enterprise Java Beans (o czym dalej).

Podsumowując: w środowisku rozproszonych aplikacji biznesowych JNDI jest podstawowym sposobem identyfikacji zasobów (baz danych, obiektów EJB itd.) oraz dostępu do nich. Dzięki temu kody aplikacji mogą być odizolowane od konkretnych warunków i konfiguracji wdrożeniowych.




4. Zdalne wywołanie metod - RMI-IIOP

Remote Method Invocation over Internet Inter-ORB Protocol (RMI-IIOP)- jest wersją RMI:


RMI
transport:  JRMP (Java Remote Method Protocol)  IIOP - TCP/IP
Znany z wykładów "Metody programowania"
(tylko Java - i to jest ograniczenie)
Umożliwia komunikację obiektów spełniająych standard CORBA (a więc nie tylko napisanych w Javie) ze zdalnymi obiektami Javy
W stosunku do JRMP:
  • brak dynamicznych  class-loaderów,
  • nie ma rozproszonego automatycznego odśmiecania,
  • do identyfikacji obiektów nie stosujemy rejestru RMI, ale JNDI realizowane przez serwis COSnaming.


Występują dwa podstawowe problemy przy interakcji pomiędzy obiektami w środowiskach rozproszonych:


Stąd szereg niezbędnych reguł przy programowaniu w RMI-IIOP:

  1. Wywołania metod na rzecz obiektów zdalnych odbywają się wyłącznie w kategoriach interfejsów. Czyli klasa naszego zdalnego obiektu musi koniecznie implementować interfejs, w którym wyszczególnione zostały zdalne metody.
  2. Ten interfejs musi rozszerzać interfejs java.rmi.Remote.
  3. Metody tego interfejsu winny być deklarowane jako zgłaszające wyjątek java.rmi.RemoteException (ponieważ przy ich zdalnym wywołaniu może nastąpić jakiś błąd komunikacji sieciowej, skutkujący powstaniem właśnie takiego wyjątku).
  4. Klasa zdalnego obiektu oprócz implementacji zdalnego interfejsu winna "eksportować" zdalny obiekt - czyli uczynić go zdolnym do przyjmowania "zleceń" - zdalnych wywołań metod. Można to zrobić poprzez    odziedziczenie klasy  javax.rmi.PortableRemoteObject (zapewniając  aby przy tworzeniu jej obiektów był wywoływany konstruktor nadklasy) albo  eksportować zdalny obiekt za pomocą statycznej metody exportObject() z klasy PortableRemoteObject. Eksportowanie obiektu oznacza jego interakcję z usługami sieciowego, dlatego zarówno wywołanie (jawne lub niejawne) konstruktora nadklasy jak i exportObject może - np. w przypadku awarii sieci - powodować powstanie wyjątku RemoteException. Dlatego sygnatura konstruktora klasy zdalnej musi uwzględniać ten wyjątek.

Przykład:

import java.rmi.*;
import javax.rmi.*;

// Interfejs

public interface AddressInfoInterface extends Remote {

   public String getAddress(String name) throws RemoteException;

}

// Implementacja

public class AddressInfo extends PortableRemoteObject
                         implements AddressInfoInterface {

   public AddressInfo(....)  throws RemoteException {
     super();  // będzie wołany automatycznie, ale specjalnie zaznaczam!
     //.....
   }

   public String getAddress(String name) throws RemoteException {
     // ...
   }
}

Uwaga: PortableRemoteObject pochodzi z pakietu javax.rmi

W ten sposób mamy klasę realizującą "zdalne obiekty". Do obiektów tej klasy można sie odwoływać w środowiskach rozproszonych. Ale trzeba powiedzieć o jaki obiekt chodzi!!!

I tu na pomoc przychodzi JNDI i serwisy nazw - w szczególności COS naming.
Po utworzeniu obiektu powinniśmy zarejestrować go pod jakąś nazwę w JNDI. Zwykle będzie tego dokonywał odrębny program - coś jakby serwer, udostępniający zdalne obiekty na zewnątrz (tak naprawdę będzie on nie tyle serwerem, co pośrednikiem w komunikacji z jakimś prawdziwym serwerem nazw). Np.

import javax.naming.*;

public class AddressInfoServer {

    public static void main(String[] args) {
        try {
            // Utworzenie zdalnego obiektu
            AddressInfo ref =
                  new AddressInfo(...);

            // Rejestracja obiektu w serwisie nazw pod nazwą
            // AddressInfoService
            // Uwaga: konkretny inicjalny kontekst określą właściwości systemowe

            Context ctx = new InitialContext();
            ctx.rebind("AddressInfoService", ref );

         } catch (Exception exc) {
            e.printStackTrace();
         }
     }
}

Z kolei zdalne wywołanie metod po stronie klienta wymaga:
po czym możemy - za pośrednictwem interfejsu - wołać metody na rzecz zdalnego obiektu.

Np.
import javax.rmi.*;
import javax.naming.*;

public class AddressInfoClient {

    public static void  main( String args[] ) {

        try {
            Context ctx = new InitialContext();

            Object objref = ctx.lookup("AddressInfoService");

            AddressInfoInterface aif; // uwaga: zawsze interfejs!
            aif = (AddressInfoInterface) PortableRemoteObject.narrow(
                                   objref, AddressInfoInterface.class);

        // zdalne wywolanie metod
        String name = "Kowalski Jan";
        String adres = aif.getAddress(name);
        System.out.println(name + " - adres: " + adres);


        } catch( Exception e ) {
            e.printStackTrace( );
        }
    }
}

Tak to wygląda z punktu widzenia programisty.
Ale jak to jest możliwe, że metody są wołane  - przecież zdalny obiekt  nie istnieje po stronie klienta, a wiec na rzecz jakiego obiektu wołamy metody?


5. RMI-IIOP od środka

Rolę odbiorcy naszych komunikatów bierze na siebie tzw. namiastka (stub). Programista - twórca zdalnego obiektu musi taką namiastkę utworzyć i zapewnić, że będzie ona dostępna po stronie klienta. Na szczęście jest to łatwe - namiastka jest tworzona automatycznie przez kompilator rmic (z opcją -iiop). Kompilator ten tworzy - jako namiastkę - klasę o nazwie _nazwaInterfejsu_Stub (w naszym przykładzie _AddressInfoInterface_Stub).

Łatwo się o tym przekonać, dodając od kodu klienta wydruk nazwy klasy obiektu otrzymanego z  JNDI:

            Object objref = ctx.lookup("AddressInfoService");
            System.out.println(objref.getClass().getName());
Uzyskamy:

_AddressInfoInterface_Stub




Czyli po stronie klienta odwołania idą do namiastki. A co robi namiastka?
Możemy podejrzeć. Oto zdekompilowany kod:

import java.rmi.RemoteException;
import java.rmi.UnexpectedException;
import javax.rmi.CORBA.Stub;
import javax.rmi.CORBA.Util;
import org.omg.CORBA.SystemException;
import org.omg.CORBA.portable.ApplicationException;
import org.omg.CORBA.portable.InputStream;
import org.omg.CORBA.portable.ObjectImpl;
import org.omg.CORBA.portable.RemarshalException;
import org.omg.CORBA.portable.ServantObject;
import org.omg.CORBA_2_3.portable.OutputStream;

public class _AddressInfoInterface_Stub extends Stub
    implements AddressInfoInterface
{

    public _AddressInfoInterface_Stub()
    {
    }

    public String[] _ids()
    {
        return _type_ids;
    }

    static Class _mthclass$(String s)
    {
        try
        {
            return Class.forName(s);
        }
        catch(ClassNotFoundException classnotfoundexception)
        {
            throw new NoClassDefFoundError(classnotfoundexception.getMessage());
        }
    }

    public String getAddress(String s)
        throws RemoteException
    {
        if(!Util.isLocal(this))
            try
            {
                org.omg.CORBA_2_3.portable.InputStream inputstream = null;
                try
                {
                    String s2;
                    try
                    {
                        OutputStream outputstream = (OutputStream)_request("getAddress", true);
                        outputstream.write_value(s, java.lang.String.class);
                        inputstream = (org.omg.CORBA_2_3.portable.InputStream)_invoke(outputstream);
                        String s1 = (String)inputstream.read_value(java.lang.String.class);
                        return s1;
                    }
                    catch(ApplicationException applicationexception)
                    {
                        inputstream = (org.omg.CORBA_2_3.portable.InputStream)applicationexception.getInputStream();
                        String s4 = inputstream.read_string();
                        throw new UnexpectedException(s4);
                    }
                    catch(RemarshalException _ex)
                    {
                        s2 = getAddress(s);
                    }
                    return s2;
                }
                finally
                {
                    _releaseReply(inputstream);
                }
            }
            catch(SystemException systemexception)
            {
                throw Util.mapSystemException(systemexception);
            }
        ServantObject servantobject = _servant_preinvoke("getAddress", AddressInfoInterface.class);
        if(servantobject == null)
            return getAddress(s);
        try
        {
            Throwable throwable1;
            try
            {
                String s3 = ((AddressInfoInterface)servantobject.servant).getAddress(s);
                return s3;
            }
            catch(Throwable throwable)
            {
                throwable1 = (Throwable)Util.copyObject(throwable, _orb());
            }
            throw Util.wrapException(throwable1);
        }
        finally
        {
            _servant_postinvoke(servantobject);
        }
    }

    private static final String _type_ids[] = {
        "RMI:AddressInfoInterface:0000000000000000"
    };

}


Nie wchodząc w szczegóły widać wyraźnie, że komunikacja sieciowa odbywa się za pomocą strumieni (na niskim poziomie są to strumienie związane z gniazdami), po których przepływają dane uformowane wg określonych reguł (protokołu). Nie ma w tym nic tajemniczego - sami byśmy zrobili podobnie.

Po stronie serwera udostępniającego obiekt zdalny te - symboliczne w istocie - zapisy oraz serializowane obiekty- argumenty muszą być przez coś odczytane, zinterpretowane i "przerobione" na faktyczne odwołania do znajdującego się tam zdalnego obiektu. Tę rolę pełni łącznik (tie), również generowany przez rmic.

Zobaczmy co robi łącznik.

import java.rmi.Remote;
import javax.rmi.CORBA.Tie;
import org.omg.CORBA.BAD_OPERATION;
import org.omg.CORBA.ORB;
import org.omg.CORBA.Object;
import org.omg.CORBA.SystemException;
import org.omg.CORBA.portable.ResponseHandler;
import org.omg.CORBA.portable.UnknownException;
import org.omg.CORBA_2_3.portable.InputStream;
import org.omg.CORBA_2_3.portable.ObjectImpl;
import org.omg.CORBA_2_3.portable.OutputStream;

public class _AddressInfo_Tie extends ObjectImpl
    implements Tie
{

    public _AddressInfo_Tie()
    {
        target = null;
    }

    public String[] _ids()
    {
        return _type_ids;
    }

    public org.omg.CORBA.portable.OutputStream _invoke(String s, org.omg.CORBA.portable.InputStream inputstream, ResponseHandler responsehandler)
        throws SystemException
    {
        try
        {
            InputStream inputstream1 = (InputStream)inputstream;
            if(s.equals("getAddress"))
            {
                String s1 = (String)inputstream1.read_value(java.lang.String.class);
                String s2 = target.getAddress(s1);
                OutputStream outputstream = (OutputStream)responsehandler.createReply();
                outputstream.write_value(s2, java.lang.String.class);
                return outputstream;
            } else
            {
                throw new BAD_OPERATION();
            }
        }
        catch(SystemException systemexception)
        {
            throw systemexception;
        }
        catch(Throwable throwable)
        {
            throw new UnknownException(throwable);
        }
    }

    public void deactivate()
    {
        _orb().disconnect(this);
        _set_delegate(null);
        target = null;
    }

    public Remote getTarget()
    {
        return target;
    }

    public ORB orb()
    {
        return _orb();
    }

    public void orb(ORB orb1)
    {
        orb1.connect(this);
    }

    public void setTarget(Remote remote)
    {
        target = (AddressInfo)remote;
    }

    public org.omg.CORBA.Object thisObject()
    {
        return this;
    }

    private AddressInfo target;
    private static final String _type_ids[] = {
        "RMI:AddressInfoInterface:0000000000000000"
    };

}
Aha, otrzymuje zlecenie - niemal jak zwykły serwer, który moglibyśmy napisać na gniazdach. Aha - deserializuje obiekty-argumenty. I najważniejsze: wywołuje metody na rzecz prawdziwego obiektu:
String s2 = target.getAddress(s1);
oraz zapisuje z powrotem wyniki.


Zatem komunikacja wygląda tak:

r


To wszakże nie znaczy, że po stronie serwera nie jest potrzebna namiastka. Zwróćmy uwagę od serwisu nazw klient - po nazwie - uzyskuje referencję właśnie do namiastki. Zatem  namiastka musi być przez serwer zarejestrowana pod określoną nazwą - przez context.bind(..) czy też rebind(). Zatem nie tylko łącznik (tie), ale i klasa namiastki musi istnieć po stronie serwera
Pamiętajmy też że po stronie klienta potrzebny jest interfejs.



6. Uruchomienie przykładowej aplikacji


Kolejne kroki w budowie i uruchomieniu przykładowej aplikacji:

  1. Stworzyć interfejs (rozszerzający Remote) i określić metody zdalne (muszą deklarować sygnalizację wyjątku RemoteException)
  2. Implementować interfejs w klasie zdalnego obiektu
  3. Zapewnić eksport zdalnego obiektu (czy to przez dziedziczenie czy przez użycie metody exportObject w tej czy innej klasie)
  4. Skompilować interfejs i  implementację (AddressInfoInterface,java i AddressInfo.java)
  5. Użyć rmic z opcją -iiop i argumentem - klasą implementacji do wyprodukowania klas namiastki (powstanie _AddressInfoInterface_Stub.class) i łącznika (powstanie: _AddressInfo_Tie.class)
rmic -iiop AddressInfo
  1. Napisać i skompilować "serwer" tworzący i rejestrujący w JNDI zdalny obiekt (zob. poprzedni wydruk AddressInfoServer.java). To tutaj zwykle następuje eksport (przez wywołanie konstruktora lub metody exportObject) no i nadanie nazwy obiektowi.
  2. Napisać i skompilować oprogramowanie wywołujące zdalnie metody obiektu (AddressInfoClient).
  3. Po stronie serwera umieścić:
  4. Po stronie klienta umieścić:
  5. Uruchomić serwer ORB (orbd.exe), który m.in. dostarcza serwisu nazw COS naming (jako argument należy podać wybrany port - najlepiej jakiś wysoki numer; można też wyspecyfikowac host, domyślnie mamy localhost)
start orbd -ORBInitialPort 3333
  1. Uruchomić serwer, definiując przy tym konfigurację JNDI (kto jest dostarczycielem inicjalnego kontekstu i jak jest dostępny serwer nazw). Możemy tę konfigurację podać w pliku jndi.properties lub jako właściwości systemowe przy uruchamianiu wirtualnej maszyny Javy: java -D... (uwaga: provider to  serwer ORB, zatem i host i port powinien być taki sam jak przy starcie serwera ORB):
java.naming.factory.initial=com.sun.jndi.cosnaming.CNCtxFactory
java.naming.provider.url=iiop://localhost:3333 
  1.  Uruchomić klienta, podając taką samą konfigurację JNDI.

Zobacz prezentację omówionych kroków.

Warto wiedzieć, że w RMI-IIOP argumenty mogą być przekazywane przez wartość i przez referencję.
W przypadku przekazywania przez wartość (to jest domyślny tryb) obiekty są serializowane i kopiowane przez sieć - zatem ew. zmiany stanów obiektu-argumentu dokonują się na kopii i nie dotyczą oryginału.

Przekazywanie argumentów przez referencję wymaga aby obiekty-argumenty były same obiektami zdalnymi (czyli implementowały Remote itd.).  Dzięki temu możemy odnosić się nie do kopii, a do oryginału - bo zdalne wywołanie metod na jego rzecz staje się możliwe.
W ten sposób zdalny obiekt na rzecz którego wywołano metodę z argumentem-zdalnym obiektem może na rzecz argumentu zdalnie wywoływać metody. Oczywiście, potrzebne są odpowiednie namiastki i łączniki, a środowisko staje się bardziej symetryczne: po trochu każdy jest tu i serwerem i klientem.


7. Zadania

Zad. 1

Wyszukać w Internecie różne rodzaje serwisów JNDI. Przetstować rozwiązania freeware'owe.

Zad. 2

Przywołać  (z wykładów MPR) aplikację PhoneDirectoryServer napisaną w konwencji klient-serwer na gniazdach.
Przerobić ją na RMI-IIOP.
Porównać nakłady pracy przy jednym i przy drugim podejściu.