Kapitel 6 Remote Method Invocation (RMI)

Det er relativt nemt at få programmer på to computere forbundet i netværk til at kommunikere ved hjælp af Java. Det er altså ikke at etablere kommunikationen, der er kompliceret B det er at bruge den fornuftigt. Samtale er ikke meget værd, hvis ikke begge parter snakker et sprog, den anden kan forstå. Det betyder, at der er behov for en eller anden form for protokol, der specificerer, hvad klienten mener, når den sender en vis besked. Protokollen skal også angive, hvilke svarmuligheder serveren har B det skal være muligt at kommunikere en fornuftig fejlmeddelelse tilbage til klienten, hvis den ønskede service ikke var tilgængelig.

Du læser en gammel bog
Den bog, du læser her, er fra 1998, og mange ting kan have ændret sig siden da.
Vi håber, at du stadig kan finde relevant information i den.
Hvis du vil læse aktuelle oplysninger om de avancerede dele af Java, anbefaler vi
bogen Core Java – Advanced Features

Sædvanligvis håndteres fejlsituationer i Java ved hjælp af exceptions. Hvis kommunikationen foregår gennem streams, er det ikke muligt at sende en exception, hvis der opstår en fejl på serveren. Hvis serveren selv kaster en exception, vil klienten ikke få det at vide, og fejlen vil blive udskrevet på serverens konsol. Den eneste løsning er at sende en tekstbesked, der angiver, at der er sket en exception, og derefter få klientprogrammet selv til at instantiere og kaste en exception. Dette undergraver imidlertid selve ideen med exceptions B fordelen skulle jo netop være, at man var fri for at kontrollere, at en bestemt operation var gået godt.

 

Et andet problem er opbygningen af serveren. Her vil der typisk være en funktion, der håndterer den indledende kommunikation med klienten. Denne funktion finder ud af, hvilken service klienten gerne vil have udført, og herefter kalder den den tilsvarende funktion. Denne funktion beder om eventuelle parametre, og sender svar tilbage til klienten.

 

Denne fremgangsmåde giver to problemer. Dels er det ikke designmæssigt pænt at have en funktion, der blot består af en række if-sætninger, der afgør, hvilken funktion, der skal kaldes bagefter, men det er også irriterende at have netværkskommunikationen spredt udover hele programmet.

 

Også for klienten ser det skidt ud. Igen bliver netværkskommunikationen spredt ud over hele programmet, og hver gang man skal have udført en kommando hos serveren, skal man instantiere en Socket, streams og så videre.

 

Det ville være nemmere, hvis man bare kunne instantiere et objekt på serveren, og herefter lade klienten kalde dem, som lå de på den lokale maskine. Dette kan man ved at bruge RMI B næsten. Der er lige et par kunstgreb, der skal gøres, men alt i alt er det væsentligt lettere end den sædvanlige kommunikation.


Serveren

 

Som nævnt kan man ved RMI lade serveren instantiere et objekt, og herefter lade klienten kalde dets metoder. Den klasse, objektet tilhører, skal imidlertid opfylde nogle bestemte krav. For eksempel skal alle de metoder, der skal kunne tilgås fra klienten, lægges ud i et interface, som klassen naturligvis skal implementere.

 

Interfacet skal nedarve fra java.rmi.Remote. Dette sikrer, at man kan behandle alle RMI-objekter abstrakt ved blot at referere til dem som havende typen java.rmi.Remote. Derudover skal man være opmærksom på, at alle metoder skal have java.rmi.RemoteException i deres throws-sektion udover de andre exceptions, de kan kaste.

 

Nedenstående eksempel viser et sådant interface. Det angiver fire matematiske funktioner. I praksis vil man nok vælge nogle lidt mere komplicerede funktioner, for den tid, der bruges på kommunikationen, er mangefold større end den tid, selve udregningen tager.

 

import java.rmi.*;

 

public interface MathInterface extends Remote

{

public float add (float a, float b) throws RemoteException;

public float sub (float a, float b) throws RemoteException;

public float mul (float a, float b) throws RemoteException;

public float div (float a, float b) throws RemoteException,

ArithmeticException;

}

 

Når interfacet er lavet, kan man begynde at skrive selve implementateringen. Den klasse, der implementerer interfacet skal nedarve fra java.rmi.server.UnicastRemoteObject. Derudover skal den have en constructor B også selvom denne er tom. Constructoren skal B ligesom de øvrige funktioner B have java.rmi.RemoteException i throws-sektionen. Dette eksempel viser implementeringen af interfacet.

 

import java.rmi.*;

import java.rmi.server.*;

 

public class MathServer extends UnicastRemoteObject implements MathInterface

{

public MathServer () throws RemoteException

{

}

 

public float add (float a, float b) throws RemoteException

{

return a + b;

}

 

 

public float sub (float a, float b) throws RemoteException

{

return a – b;

}

 

public float mul (float a, float b) throws RemoteException

{

return a * b;

}

 

public float div (float a, float b) throws RemoteException,

ArithmeticException

{

return a / b;

}

}

 

Nu er selve den klasse, der skal gøres tilgængelig skrevet. Nu skal der blot skrives en klasse, der kalder de funktioner, der registrerer klassen som offentlig tilgængelig. Den kode kunne naturligvis have ligget i MathServer=s main-funktion, men her er det altså valgt at lave en selvstændig klasse til det. Nedenstående eksempel viser, hvordan et MathServer-objekt instantieres og registreres.

 

import java.rmi.*;

import java.net.MalformedURLException;

 

public class Server

{

public static final void main (String args[])

{

System.setSecurityManager (new RMISecurityManager());

 

try

{

MathServer obj = new MathServer ();

}

catch (RemoteException re)

{

System.out.println (re.toString());

System.exit (1);

}

 

try

{

Naming.bind (“//void-main/Math”, obj);

System.out.println (“Udført”);

}

catch (MalformedURLException mue)

{

System.out.println (mue.toString());

 

}

catch (UnknownHostException uhe)

{

System.out.println (uhe.toString());

}

catch (AlreadyBoundException abe)

{

System.out.println (abe.toString());

}

}

}

 

Det første, der sker i eksemplet er, at der bliver installeret en security manager. Det kan enten være RMISecurityManager, eller en man selv har defineret. Installerer man ikke en security manager, er det ikke tilladt at instantiere RMI-klasser.

Dernæst instantieres et objekt af klassen MathServer. Som nævnt tidligere, kan det medføre en RemoteException, og derfor bliver den fanget her. Opstår denne exception afbrydes programmet, for så er der ikke megen idé i at forsøge at registrere objektet.

Hvis objektet bliver instantieret korrekt, er det næste skridt at gøre det tilgængeligt for klienterne. Det gøres ved at registrere det i Javas Naming-service ved hjælp af metoden bind, der er en statisk metode i klassen Naming. Det første parameter er en tekststreng, der angiver den URL, objektet skal kendes under. Klienterne skal kende denne URL for at kunne tilgå objektet. Den første del af URL=en er computerens navn B i dette tilfælde void-main. Herefter skrives det navn, objektet skal være kendt under. Det behøver ikke at have nogen tilknytning til klassens eller objektets navn i programmet. Metodens andet parameter er en reference til det objekt, der skal bindes. Dette objekt skal være en sub-klasse af java.rmi.server.UnicastRemoteObject.

 

Da det første parameter som nævnt er en URL, kan bind-metoden resultere i en java.net.MalformedURLException. Den kan også medføre en java.rmi.UnknownHostException, hvis navnet på computeren ikke er gyldigt. Endeligt kan funktionskaldet medføre en java.rmi.AlreadyBoundException, hvis der allerede er bundet et objekt med det navn, man angiver. Hvis man ved, at man har bundet objektet tidligere, men gerne vil opdatere det, kan man bruge Naming.rebind i stedet for Naming.bind.

Et objekt, der nedarver fra java.rmi.server.UnicastRemoteObject behøver ikke at blive registreret i naming-servicen. Hvis en anden funktion returnerer objektet, kan klienter sagtens kalde funktioner på det, selvom det ikke er registreret. Problemet er bare at få fat i den første instans af et objekt. Dette objekt kan ikke returneres ved et funktionskald, for man har ikke noget objekt at kalde funktionen på. I stedet kan naming-servicen bruges som vist i dette eksempel. Bemærk, at selvom man skal angive navnet på en computer i bind-metoden, er det kun muligt at bruge naming-servicen på den lokale maskine. Dette er gjort for at sikre, at man ikke kan tilslutte sig en anden computers naming-service, registrere et af sine egne objekter i stedet for det, der tidligere var registreret, og derefter få klienter til at tro, at de bruger det originale objekt.

 

 

Når kildeteksterne er skrevet kan de kompileres med den vanlige Java-compiler. Derefter skal den klasse, der skal være offentligt tilgængelig, kompileres med en speciel RMI-compiler. Hvis man har Suns JDK hedder den rmic. Det vil altså sige, at man fra kommandoprompten skal skrive rmic MathServer for at få klassen MathServer gennem RMI-compileren. Når rmic er færdig har den genereret to nye klasser: MathServer_Stub og MathServer_Skel. Stub-klassen skal lægges sammen med klient-programmet B det er den, der håndterer netværkskommunikationen med Skeleton-klassen, der lægges på serveren. Set fra programmørens synspunkt er dette ikke så vigtigt B man får aldrig selv brug for at instantiere eller på anden måde benytte stub- og skeleton-klasserne i sit program.

 

Nu hvor programmet er kompileret og alle klassefiler genereret, er det klar til udførsel. Inden programmet startes skal man dog starte RMI-registreringsdatabasen. Det er program, der følger med Suns JDK. Programmet startes med kommandoen rmiregistry. Man skal huske, enten at starte rmiregistry fra det bibliotek, hvor klassefilerne ligger eller at inkludere klassefilerne i sin classpath.

 

Klienten

 

Når server-programmet er færdig, kan man begynde at skrive klienten. Klienten har to RMI-relaterede opgaver. Den skal for det første finde det rette objekt, og for det andet skal den kalde metoder på objektet.

 

Den første opgave løses ved hjælp af naming-servicen. Klienten kender servermaskinens navn og også det navn, objektet er blevet registreret som. Disse oplysninger sendes videre til naming-servicen ved hjælp af lookup-metoden, der er en statisk funktion i Naming-klassen. Denne funktion returnerer en instans af RMI-objektet. Alle referencer til RMI-objekter, der bruges som parametre eller returværdier, skal være referencer til interfacet og ikke til selve objektet. Derfor skal klientens reference til MathServer være af typen MathInterface. Da Naming.lookup er en generel funktion, skal returværdien castes.

 

lookup-funktionen kan ende med en del exceptions. Den første er java.net.MalformedURLException, der opstår, hvis adressen på objektet er formuleret forkert. Dernæst kan java.rmi.NotBoundException opstå. Det sker, hvis det objekt, man forsøger at få kontakt med, ikke er bundet på serveren. Endeligt kan java.rmi.RemoteException blive opstå, hvis der opstår en generel kommunikationsfejl.

 

Når først naming-servicen har fundet objektet og returneret en reference til det, er det klart til brug. Man skal bare huske, at ethvert kald af en funktion i et RMI-objekt kan medføre en java.rmi.RemoteException B derfor skal man enten erklære den i funktionernes throws-liste eller catch=e dem hver gang. Nedenstående klasse viser, hvordan MathServer kan bruges.

 

import java.rmi.*;

 

public class MathClient

{

public static final void main (String args[])

 

{

MathInterface math = null;

 

try

{

math = (MathInterface) Naming.lookup (“//void-main/Math”);

}

catch (java.net.MalformedURLException mue)

{

System.out.println (mue.toString());

System.exit (1);

}

catch (NotBoundException nbe)

{

System.out.println (nbe.toString());

System.exit (2);

}

catch (RemoteException re)

{

System.out.println (re.toString());

System.exit (3);

}

 

 

try

{

System.out.println (“1 + 2 er ” + math.add (1,2));

System.out.println (“3 – 4 er ” + math.sub (3,4));

System.out.println (“5 * 6 er ” + math.mul (5,6));

System.out.println (“7 / 8 er ” + math.div (7,8));

}

catch (RemoteException re)

{

System.out.println (re.toString());

System.exit (4);

}

}

}

 

Fordeling af filer

 

Når både serveren og klienten er skrevet og kompileret, er programmet parat til at blive delt ud på forskellige maskiner. På serveren skal implementeringen af server-klassen ligge sammen med interfacet, og den skeleton-klasse, der blev genereret af rmi-compileren. På klienten skal klient-programmet naturligvis ligge. Det skal interfacet også og den stub-klasse, rmi-compileren genererede. En samlet oversigt over placeringen af filer ses nedenstående tabel

 

 

 

Fil Placering
Implementering af server-klasse Server
Skeleton-klasse Server
Interface Server og klient
Stub-klasse Klient
Implementering af klient-klasse Klient

En nameserver

 

RMI gør det muligt at distribuere sin applikation over adskillige maskiner. Desværre er det uheldigt, at adressen på samtlige servere skal være kendt af klienten. Dette vil betyde ændringer i samtlige klienter, hvis bare en af serverne skiftede adresse. For at løse dette problem kan man bruge en nameserver.

Nameserverens opgave er at registrere hvilke servere, der stiller hvilke services til rådighed. Det vil sige, at alle servere skal fortælle nameserveren, at de udbyder en service. Dernæst kan en klient bede nameserveren om en reference til et givent objekt eller en given service, hvorefter nameserveren vil bede den rigtige server om en reference til objektet, der sendes videre til klienten. Der er tydeligvis nogle tidsmæssige omkostninger ved at bruge en nameserver, men nogle gange kan det være den eneste udvej.

I dette afsnit vil et nameserver-eksempel blive gennemgået. Der er tale om en relativt primitiv nameserver B der er mange sikkerhedsaspekter, der ikke er taget højde for. Dette er gjort fuldt bevidst, da disse aspekter ikke har meget med RMI at gøre og blot vil gøre kildeteksten mere kompliceret at læse og forstå. Eksemplet afsluttes imidlertid med et afsnit, der kort beskriver, hvordan de nødvendige udvidelser kan implementeres.

 

Krav til nameserveren

 

Nameserveren skal have følgende funktionalitet:

  • Servere skal kunne registrere deres services hos nameserveren.
  • Servere skal kunne fjerne deres services fra nameserveren, når de ikke længere bliver udbudt.
  • Klienter skal kunne tilgå objekter på en hvilken som helst server gennem nameserveren. Når først klienten har fået en reference til objektet på den rigtige server, skal nameserverens rolle være udspillet.

 

Disse krav kan opfyldes af funktionerne i nedenstående interface:

 

import java.rmi.*;

import java.net.MalformedURLException;

 

public interface RMINameServer extends Remote

{

public void bind(String host, String name) throws RemoteException,

AlreadyBoundException;

 

public void unbind (String host, String name) throws RemoteException;

 

public void rebind (String host, String name) throws RemoteException;

 

public Object lookup (String name) throws RemoteException,

MalformedURLException, NotBoundException;

}

 

Funktioner i nameserveren

 

bind-funktionen bruges, når en server skal registreres i nameserveren. Alle servere skal som bekendt registreres i nameserveren. Den første parameter er navnet på server-computeren. Det kan enten være serverens IP-nummer eller computerens navn på netværket. name er det navn, objektet er bundet under på serveren – objektet vil på nameserveren blive bundet på det samme navn. Klienterne bruger dette navn til at tilgå objektet.

 

unbind-funktionen bruges, når en server ophører med at stille en bestemt service til rådighed. Igen gives server-computerens navn med som parameter sammen med objektets navn.

rebind er en udvidelse af bind-funktionen. Den opdaterer den reference, der er til det angivne objekt. Hvis objektet ikke allerede er bundet i nameserveren, vil det blive bundet, som var der foretaget et kald af bind i stedet for rebind.

 

lookup bruges af klienterne til at få en reference til et givent objekt. Det navn, der gives med, er det navn, som serverne har valgt at blive bundet på.

 

Som det ses, er funktionerne stor set magen til de, der findes i Naming-klassen.

Implementeringen af RMINameServer-interfacet findes i klassen ConcreteNameServer:

 

import java.rmi.*;

import java.rmi.server.*;

import java.util.*;

import java.net.MalformedURLException;

 

public class ConcreteNameServer extends UnicastRemoteObject implements RMINameServer

{

protected Vector bindings;

 

public ConcreteNameServer () throws RemoteException

{

bindings = new Vector();

}

 

public void bind (String host, String name) throws RemoteException,

AlreadyBoundException

{

Binding b = new Binding (host, name);

 

if (bindings.contains (b))

throw new AlreadyBoundException (host + “/” + name);

 

bindings.addElement (b);

}

 

public void unbind (String host, String name) throws RemoteException

{

Binding b = new Binding (host, name);

bindings.removeElement (b);

}

 

public void rebind (String host, String name) throws RemoteException

{

try

{

unbind (host,name);

bind (host, name);

}

catch (AlreadyBoundException abe)

{

// Just catch it and ignore it

}

}

 

public Object lookup (String name) throws RemoteException,

MalformedURLException, NotBoundException

{

for (int i = 0; i < bindings.size(); i++)

{

if (((Binding) bindings.elementAt (i)).name.equals (name))

{

return Naming.lookup (“rmi://” + ((Binding) bindings.elementAt (i)).host

+ “/” + ((Binding) bindings.elementAt (i)).name);

}

}

throw new NotBoundException (name);

}

}

 

Nameserveren bruger klassen Binding til at registrere et server/objekt-par. Listen over bindinger gemmes herefter i en vektor (java.util.Vector). Binding-klassen er specielt udviklet til nameserveren:

 

public class Binding

{

public String host;

public String name;

 

public Binding ()

 

{

name = host = null;

}

 

public Binding (String host, String name)

{

this.host = host;

this.name = name;

}

}

 

Med de klasser, der indeholder nameserverens funktionalitet, mangler man bare den programkode, der opretter en instans af ConcreteNameServer-klassen og binder denne i registreringsservicen, så klienter og servere har adgang til den:

 

import java.rmi.*;

import java.rmi.server.*;

import java.net.MalformedURLException;

 

public class NameServerApp

{

public static void main(String args[]) throws MalformedURLException,

RemoteException

{

System.out.println (“Initializing”);

ConcreteNameServer nameServer = new ConcreteNameServer();

System.setSecurityManager (new RMISecurityManager ());

Naming.bind (“NameServer”, nameServer);

System.out.println (“Server ready”);

}

}

 

Et client/server eksempel med nameserver

 

For at vise, hvordan henholdsvis klienten og serveren anvender en nameserver, beskrives her, hvilke ændringer, der skal foretages i det Math-eksempel, der blev gennemgået tidligere i kapitlet. Interfacet MathInterface og klassen MathServer skal ikke ændres. Disse vedrører kun implementeringen af funktionaliteten på serveren, og den skal naturligvis være den samme, hvadenten der bruges en nameserver eller ej. Der skal til gengæld foretages ændringer i den klasse, der opretter MathServer-objektet og binder det – denne klasse skal nu også sørge for at registrere klassen i nameserveren. Det betyder, at server-klassens main-funktion kommer til at se således ud:

 

public static final void main (String args[])

{

System.setSecurityManager (new RMISecurityManager());

MathServer obj = null;

try

 

{

obj = new MathServer ();

}

catch (RemoteException re)

{

System.out.println (re.toString());

System.exit (1);

}

 

try

{

Naming.bind (“//void-main/Math”, obj);

System.out.println (“Objekt bundet lokalt”);

}

catch (MalformedURLException mue)

{

System.out.println (mue.toString());

}

catch (UnknownHostException uhe)

{

System.out.println (uhe.toString());

}

catch (AlreadyBoundException abe)

{

System.out.println (abe.toString());

}

catch (RemoteException rr)

{

System.out.println (rr.toString());

}

 

RMINameServer nameserver = null;

 

try

{

nameserver = (RMINameServer) Naming.lookup (“//nameserver/NameServer”);

}

catch (RemoteException rx)

{

System.out.println (rx.toString());

}

catch (MalformedURLException mux)

{

System.out.println (mux.toString());

}

catch (NotBoundException nbx)

{

System.out.println (nbx.toString());

}

 

try

{

nameserver.bind (“void-main”,”Math”);

 

}

catch (RemoteException rex)

{

System.out.println (rex.toString());

}

catch (AlreadyBoundException abex)

{

System.out.println (abex.toString());

}

}

 

Også klient-programmet skal ændres, men dog ikke i samme omfang som serveren. Blot skal klienten nu spørge nameserveren om at få en reference til MathServer-objektet i stedet for selve serveren. Det betyder, at main-funktionen i MathClient-klassen nu kommer til at se således ud:

 

public static final void main (String args[])

{

MathInterface math = null;

 

try

{

math = (MathInterface) Naming.lookup (“//nameserver/Math”);

}

catch (java.net.MalformedURLException mue)

{

System.out.println (mue.toString());

System.exit (1);

}

catch (NotBoundException nbe)

{

System.out.println (nbe.toString());

System.exit (2);

}

catch (RemoteException re)

{

System.out.println (re.toString());

System.exit (3);

}

 

 

try

{

System.out.println (“1 + 2 er ” + math.add (1,2));

System.out.println (“3 – 4 er ” + math.sub (3,4));

System.out.println (“5 * 6 er ” + math.mul (5,6));

System.out.println (“7 / 8 er ” + math.div (7,8));

}

catch (RemoteException re)

{

System.out.println (re.toString());

System.exit (4);

}

}

 

 

Forbedringer til nameserveren

 

Som nævnt tidligere er det muligt at foretage mange forbedringer til den nameserver, der er blevet skitseret her.

 

Sikkerheden i systemet er ikke-eksisterende. Det er muligt for en hvilken som helst server at unbind’e en anden servers objekt og i stedet indsætte sit eget objekt i det samme navn. Denne risiko kan man minimere ved at lade bind- og rebind-funktionerne returnere et unikt id. Dette id skal sendes med til unbind og rebind for at sikre, at det er den samme server, der bind’er og unbind’er objekter.

Et andet problem er, at serverne skal kalde unbind-funktionen for at vise, at de ikke længere stiller deres service til rådighed. Desværre er det jo engang sådan, at computere af og til går ned uden videre, og man kan derfor risikere, at nameserveren har referencer til computere, der er gået ned. Denne problematik kan afhjælpes ved hjælp af time-outs. Ved at lade serveren kontrollere, om der stadig er kontakt til alle serverne med jævne mellemrum, kan man formindske antallet af ”døde” servere drastisk. Det kan gøres ved at kræve, at alle objekter skal inkludere et interface, der indeholder funktionen ping(). Det eneste, denne funktion skal gøre, er at returnere true for at vise, at objektet stadig eksisterer, og at server-computeren stadigvæk kører. Hvis nameserveren ikke modtager svaret fra funktionen inden for et bestemt tidsrum, skal det pågældende objekt fjernes fra listen over registrerede objekter. Hvor lang tid serverne skal have til at svare, afhænger af netværkets hastighed og belastning. Hvor ofte nameserveren skal kontrollere om de øvrige servere er i live, er en afvejning mellem, hvor meget man ønsker at belaste netværket, nameserveren og de øvrige servere, og hvor mange ”døde” servere, man vil acceptere, der eksisterer på nameserveren.

 

Brugerdefinerede RMI-sockets

 

RMI åbner mulighed for, at man selv kan definere sine sockets. Der kan være mange grunde til, at man selv ønsker at definere sine sockets. De to mest udbredte er

  • Man vil have klient og server til at bruge en protokol, der krypterer eller komprimerer data.
  • Man vil anvende forskellige sockets til forskellige forbindelser.

 

En mulig og særdeles populær anvendelse af brugerdefinerede RMI-sockets er SSL. Ved at få RMI til at kommunikere ved hjælp af SSL-protokollen kan man sikre sikker, krypteret kommunikation mellem klienten og serveren.

 

Man definerer sin egen socket ved at anvende en java.rmi.server.RMISocketFactory. Ved at gøre det, får man mulighed for at anvende en anden protokol end den TCP/IP, RMI normalt anvender. RMI anvender nemlig normalt den socket, der er defineret i java.net.Socket.

 

Fra og med JDK version 1.2 er det muligt at definere forskellige former for sockets til forskellige forbindelser – det gør man ved hjælp af klassen java.rmi.server.SocketType. I dette afsnit vil det blive gennemgået, hvordan man opretter en RMISocketFactory, der producerer en enkelt type

 

socket, og hvordan man laver en RMISocketFactory, der producerer flere forskellige typer.

 

Inden det gennemgås, hvordan man bruger brugerdefinerede sockets sammen med RMI, er det imidlertid en god idé at se på, hvordan man opretter en brugerdefineret socket.



 

Oprettelse af en brugerdefineret socket

 

Når man vil behandle data før man transporterer over netværket, og/eller efter man har modtaget dem, kan man gøre det på to måder. Man kan enten bruge en almindelig Java socket (java.net.Socket) og behandle dataene i selve applikationen, eller man kan definere sin egen socket, der foretager behandlingen. I dette afsnit vil den sidste fremgangsmåde blive gennemgået.

 

Overordnet kan den proces at oprette en socket opdeles i fire trin:

 

  1. Opret en klasse, der nedarver fra fra en output stream-klasse (for eksempel java.io.FilterOutputStream) for at give socket’en en stream til output. Redefinér de metoder, der ikke kan bruges, som de er.
  2. Opret en klasse, der nedarver fra en input stream-klasse (for eksempel java.io.FilterInputStream) for at give socket’en en stream til input. Redefinér de metoder, der ikke kan bruges, som de er.
  3. Opret en klasse, der nedarver fra java.net.Socket. Implementér de nødvendige constructors, og redefinér getInputStream, getOutputStream og close.
  4. Opret en klasse, der nedarver fra java.net.ServerSocket. Implementér constructoren og redefinér accept-metoden, så den opretter en socket af den rigtige type.

 

Hvilke output og input stream-klasser man vælger at nedarve fra, afhænger af den applikation man programmerer, og den type kommunikation, socketen skal bruges til.

 

For at lette gennemgangen af de ovennævnte punkter vil der blive taget udgangspunkt i et praktisk eksempel, hvor der bliver oprettet en socket til håndtering af krypteret kommunikation. Selve krypteringen er ikke medtaget i dette eksempel af hensyn til overskueligheden. I stedet kaldes funktionerne encrypt og decrypt på klassen Encryption. I en virkelig applikation skulle denne klasse foretage henholdvis kryptering og dekryptering. I dette eksempel er den blot defineret således:

 

public class Encryption

{

public static int encrypt (int b)

{

// kryptering foregår her

 

 

return b;

}

 

public static int decrypt (int b)

{

// dekryptering foregår her

 

return b;

}

}

 

Herefter er det muligt at definere en output stream, der skriver krypteret data. Denne defineres således:

 

import java.io.*;

 

public class EncryptionOutputStream extends FilterOutputStream

{

public EncryptionOutputStream (OutputStream os)

{

super (os);

}

 

public void write(int b) throws IOException

{

out.write (Encryption.encrypt(b));

}

 

public void write(byte b[], int off, int len) throws IOException

{

for (int i = 0; i < len; i++)

out.write (Encryption.encrypt(b[off + i]));

}

}

 

Som det ses, nedarver klassen fra java.io.FilterOutputStream. At det netop skulle være FilterOutputStream og ikke en anden output stream, der skulle nedarves fra, er et valg foretaget med den begrundelse, at FilterOutputStream passer bedst til den kommunikation, socketen skal bruges til.

 

For at kunne kryptere dataene bliver FilterOutputStream’s metoder til at skrive data redefineret. Det drejer sig om de to write-metoder. Den første metode, public void write(int b), krypterer og skriver en enkelt byte. Den anden metode, public void write(byte b[], int off, int len), skriver en række krypterede bytes fra arrayet b. Antallet af bytes, der skal skrives, afgøres af len-variablen, mens off angiver, positionen på den første byte i arrayet, der skal skrives.

Nu hvor output stream’en er på plads, er det på tide at få lavet en input stream til socket’en. I tråd med output stream’en er det valgt at bruge en FilterInputStream til at håndtere indlæsningen af data. Det betyder, at EncryptionInputStream kan skrives som følger:

import java.io.*;

 

public class EncryptionInputStream extends FilterInputStream

{

public EncryptionInputStream (InputStream is)

{

super (is);

 

 

}

 

public int read () throws IOException

{

return (Encryption.decrypt(in.read()));

}

 

public int read (byte b[], int off, int len) throws IOException

{

if (len <= 0)

return 0;

 

int i;

int bytesRead;

 

for (bytesRead = 0; bytesRead < len && in.available() > 0; bytesRead++)

{

try

{

i = read();

}

catch (EOFException eof)

{

return bytesRead;

}

 

b[off + bytesRead] = (byte) Encryption.decrypt(i);

}

 

return bytesRead;

}

}

 

Ligesom i EncryptionOutputStream bliver der redefineret to metoder. Her drejer det sig blot om read i stedet for write.

 

Den første read-funktion bruges til at læse en enkelt int fra input stream’en. Den anden læser flere bytes efter samme system som den write-metode, der skrev flere bytes. Denne read-metode returnerer antallet af bytes, der bliver indlæst. Dette antal kan være forskellig fra len, hvis der opstår en EOFException, hvilket sker, hvis der ikke er tilstrækkeligt data i stream’en, eller hvis der ikke kan læses flere data fra stream’en uden at blokere – det vil sige, at stream’en er tom. Man kan se, hvor mange data, der er tilgængelige i stream’en, ved at kalde available-funktionen.

 

Nu hvor man har defineret både en output stream og en input stream, kan man definere selve socketen. Denne kaldes i dette eksempel EncryptionSocket:

 

import java.io.*;

import java.net.*;

 

public class EncryptionSocket extends Socket

{

private InputStream in;

 

private OutputStream out;

 

public EncryptionSocket ()

{

super ();

}

 

public EncryptionSocket (String host, int port) throws IOException

{

super (host, port);

}

 

public InputStream getInputStream () throws IOException

{

if (in == null)

in = new EncryptionInputStream(super.getInputStream());

 

return in;

}

 

public OutputStream getOutputStream () throws IOException

{

if (out == null)

out = new EncryptionOutputStream(super.getOutputStream());

 

return out;

}

 

public synchronized void close () throws IOException

{

OutputStream o = getOutputStream();

o.flush();

super.close();

}

}

 

EncryptionSocket nedarver fra java.net.Socket. For at sikre at man kan bruge denne socket ligesom alle andre sockets – og i særdeleshed ligesom java.net.Socket – må man redifinere metoderne getInputStream og getOutputStream, så disse ikke blot returnerer en almindelig input og output stream, men henholdsvis EncryptionInputStream og EncryptionOutputStream.

 

Både getInputStream og getOutputStream kontrollerer, om der i forvejen eksisterer en input stream eller output stream. Er dette tilfældet, returneres denne. Eksisterer der ikke i forvejen en stream, bliver der oprettet en, som derefter returneres.

 

Den sidste metode i EncryptionSocket er close. Denne metode sørger for at tømme output streamen og kalder så videre til superklassens close-metode.

 

 

Det sidste, der skal gøres, før den brugerdefinerede socket er klar til brug, er at definere en ServerSocket. Denne kaldes EncryptionServerSocket, og definitionen af den ses herunder.

 

import java.io.*;

import java.net.*;

 

public class EncryptionServerSocket extends ServerSocket

{

public EncryptionServerSocket (int port) throws IOException

{

super (port);

}

 

public Socket accept () throws IOException

{

Socket s = new EncryptionSocket();

implAccept(s);

return s;

}

}

 

Igen skal der redefineres en metode. Denne gang er det accept. I stedet for at returnere en almindelig socket, skal funktionen nu returnere en EncryptionSocket.

 

Således er det muligt at definere sin egen socket. Det skal dog understreges, at dette eksempel er simplere end de fleste, da Encryption-protokollen ligger ovenpå TCP/IP. Det betyder, at protokollen kan bruge de fleste af de metoder, der allerede er defineret i input og output streams og sockets. Havde det været en helt anden protokol, der skulle have været implementeret, ville det have krævet en større mængde programmering og redefinering af de fleste allerede eksisterende metoder.

En RMISocketFactory, der producerer en enkelt type socket

 

Der er fire trin, der skal gennemgås for at kunne oprette en RMISocketFactory, der producerer en enkelt sockettype:

 

  1. Vælg, hvilken type socket, der skal produceres.
  2. Opret en klasse, der nedarver fra klassen RMISocketFactory.
  3. Redefinér metoden createSocket.
  4. Redefinér metoden createServerSocket.

 

Hvilken sockettype man vælger at implementere, afhænger naturligvis af den applikation, man skriver. Hvis applikationen arbejder med særdeles følsomme data, er det en god idé at bruge en socket, der krypterer data, mens applikationer, der arbejder med store datamængder, med fordel vil kunne bruge en socket, der tillader datakomprimering. Man kan enten vælge at anvende en socket, man har programmeret tidligere, programmere en ny
socket specielt til lejligheden eller bruge en socket, der leveres af en tredjepart. I dette eksempel vil der blive arbejdet videre med den EncryptionSocket, der blev defineret i det foregående afsnit.

 

Når socketvalget er foretaget skal der oprettes en klasse, der nedarver fra RMISocketFactory. I dette tilfælde kaldes den EncryptionRMISocketFactory. Denne er defineret som følger.

 

import java.io.*;

import java.net.*;

import java.rmi.server.*;

 

public class EncryptionRMISocketFactory extends RMISocketFactory

{

public Socket createSocket (String host, int port) throws IOException

{

EncryptionSocket socket = new EncryptionSocket (host, port);

return socket;

}

 

public ServerSocket createServerSocket (int port) throws IOException

{

EncryptionServerSocket serverSocket = new EncryptionServerSocket (port)

return serverSocket;

}

}

 

Som det ses, er også skridt 3 og 4 foretaget, idet createSocket og createServerSocket er blevet redefineret. Formålet med en RMIFactory er, at den skal forsyne RMI-systemet med sockets, når programmet afvikles. Derfor er det nødvendigt at redefinere createSocket-metoden. Også createServerSocket skal redefineres, så også serverdelen af en RMI-kommunikation kan benytte den korrekte socket.

 

Nu hvor det er blevet gennemgået, hvordan man opretter en RMISocketFactory, der kan producere en enkelt type af sockets, kan man gå videre med at se på, hvordan man laver en RMISocketFactory, der kan producere flere forskellige sockets. Det vil blive gennemgået i det næste afsnit.

 

En RMISocketFactory, der producerer flere forskellige typer sockets

 

At oprette en RMISocketFactory, der producerer flere forskellige typer sockets, minder meget om at oprette en RMISocketFactory, der blot producerer én type sockets. Man behøver kun en smule mere information.

 

Hvis en RMISocketFactory kan producere mere end én type sockets, skal

 

den have en måde at angive, hvilken type der skal produceres. Udover de metoder, der blev brugt i den foregående RMISocketFactory, nemlig

 

public Socket createSocket (String host, int port) og

public ServerSocket createServerSocket (int port)

 

definerer RMISocketFactory også to metoder, der gør det muligt at angive, hvilken type socket, der skal produceres. Disse har følgende signatur:

 

public Socket createSocket (String host, int port, SocketType type)
public ServerSocket createServerSocket (int port, SocketType type)

 

Som det ses, er forskellen på disse to og de, der blev anvendt i det forrige eksempel, at de to nye metoder, hver har en parameter, der angiver hvilken type socket, der skal oprettes. Klassen SocketType har tre variable:

 

private String protocol
private byte[] refData
private Object serverData

 

Disse tre elementer sættes af klassens constructor. Elementet protocol angiver den protokol, der anvendes i kommunikationen. De to øvrige elementer kan bruges til at opbevare data, der kan være nødvendige for protokollen.

 

Med denne viden i baghånden kan man bruge fremgangsmåden med de 4 trin, der blev skitseret i forrige afsnit.

 

Først vælges, hvilke sockets der skal kunne produceres. For eksemplets skyld vælges her EncryptionSocket og java.net.Socket. Den RMISocketFactory, der skal oprette en eller flere af disse to sockets, kaldes DualRMISocketFactory.

 

import java.io.*;

import java.net.*;

import java.rmi.server.*;

 

public class DualRMISocketFactory extends RMISocketFactory

{

private RMISocketFactory defaultFactory = RMISocketFactory.getDefaultSocketFactory();

 

public Socket createSocket(String host, int port) throws IOException

{

return defaultFactory.createSocket (host, port);

}

 

public ServerSocket createServerSocket (int port) throws IOException

{

return defaultFactory.createServerSocket (port);

}

 

public Socket createSocket (String host, int port, SocketType type) throws IOException

 

{

Socket socket;

String protocol = type.getProtocol();

if (protocol.equalsIgnoreCase(“encryption”))

socket = new EncryptionSocket (host, port);

else

if (protocol.equalsIgnoreCase(“java”))

socket = new Socket (host, port);

else

throw new ProtocolException (“Protocol not supported : ” + protocol);

 

return socket;

}

 

public ServerSocket createServerSocket (int port, SocketType type) throws IOException

{

ServerSocket socket;

String protocol = type.getProtocol();

if (protocol.equalsIgnoreCase(“encryption”))

socket = new EncryptionServerSocket (port);

else

if (protocol.equalsIgnoreCase(“java”))

socket = new ServerSocket (port);

else

throw new ProtocolException (“Protocol not supported : ” + protocol);

 

return socket;

}

}

 

Sammenlignet med EncryptionRMISocketFactory er der en del forskelle. Begge implementerer metoderne public Socket createSocket(String host, int port) og public ServerSocket createServerSocket (int port). DualRMISocketFactory sender imidlertid blot disse metodekald videre til den almindelige RMISocketFactory, da der ikke sendes nogen SocketType med, og da det derfor ikke er muligt at bestemme, hvilken type socket, der skal oprettes. Derudover definerer DualRMISocketFactory metoderne public Socket createSocket (String host, int port, SocketType type) og public ServerSocket createServerSocket (int port, SocketType type). Disse indeholder en parameter, der gør det muligt for klassen at se, hvilken type socket, der skal oprettes. Dette gøres på baggrund af SocketType’s protocol-element. Hvis værdien af dette er ”encryption” oprettes en EncryptionSocket, mens en streng med værdien ”java” vil oprette en java.net.Socket. Er værdien noget andet end dette, opstår en exception.

 


Brug af en brugerdefineret socket i en applikation

 

Det nytter ikke meget at kunne definere smarte RMISocketFactory-klasser, hvis man ikke kan bruge dem i sine applikationer. Derfor vil det i dette afsnit blive forklaret, hvordan man sætter RMI-systemet til at bruge brugerdefinerede sockets.

 

At implementere brugerdefinerede sockets i en applikation er en proces, der kan deles op i to trin:

 

  1. Sæt RMI-factory i både klienten og serveren til den brugerdefinerede factory.
  2. Implementér en constructor i serverklassen, der kalder UnicastRemoteObject med SocketType-parametren.

 

Trin 2 er kun nødvendig, hvis man implementerer en factory, der kan producere flere forskellige typer sockets.

 

For at sætte RMI-factory i klient og server, skal der bruges en statisk metode, som er en del af RMISocketFactory-klassen. Denne metode har følgende signatur:

 

public synchronized static void setSocketFactory(RMISocketFactory fac)

 

Den kode, der sætter RMI-factory og som skal implementeres på såvel klient- som serverside, kan se således ud:

 

try

{

RMISocketFactory.setSocketFactory(new DualRMISocketFactory());

}

catch (IOException e)

{

// håndter fejl

}

 

Hvis den RMISocketFactory, man anvender, kan producere flere forskellige sockettyper, er det nødvendigt at angive, hvilken der bruges. Dette skal kun gøres på serveren, og hvis denne nedarver fra UnicastRemoteObject kan det gøres rimeligt nemt. UnicastRemoteObject har nemlig en protected constructor, hvor man blandt andet kan angive, hvilken SocketType, der anvendes. Denne constructor har følgende signatur:

 

protected UnicastRemoteObject(int port, SocketType socketType)

 

I constructoren i server-klassen kan kaldet af denne constructor, der altså findes i superklassen, se således ud:

 

super (0, new SocketType (”encryption”, null, null));

 


Sammenfatning

 

I dette kapitel er det blevet gennemgået, hvordan man kan skrive et distribueret system ved hjælp af Remote Method Invocation (RMI). Det er blevet forklaret, hvordan de forskellige klasser skal fordeles. Derudover er det blevet vist, hvordan man kan anvende mere avancerede emner indenfor RMI. Det drejer sig blandt andet om anvendelsen af en nameserver og programmereing af egne sockets.

 

FAQ

Kan man bruge RMI fra applets?

 

Ja. RMI er den letteste måde at foretage netværkskommunikation fra en applet på. Man skal blot huske, at Javas sikkerheds­politik kun tillader, at man foretager RMI-kald til den compu­ter, appleten blev indlæst fra. Man finder adressen på den computer ved at foretage kaldet getCodeBase().getHost(). Det vil sige, at en typisk søgning i Naming-servicen kommer til at se således ud:

 

RemoteInterface obj = (RemoteInterface) Naming.lookup (“//” + getCodeBase().getHost() + “/Remote”);

 

Prorammet siger, at den ikke kan finde en computer med ser­verens navn, men navnet er rigtigt. Hvad er der galt?

 

Hvis man forsøger at få kontakt med en computer på et andet netværk – for eksempel via Internet – kan det være nødvendigt at angive computerens IP-nummer i stedet for dens navn. Det samme er tilfældet, hvis den bruger, der er logget ind på servermaskinen, ikke er logget ind på netværket.

 

Kan man køre to registreringsservices på samme computer?

 

Ja. Hvis man har brug for at stille forskellige objekter til rådighed for forskellige servere, kan man med fordel vælge at køre to registreringsservices i stedet for blot en enkelt. Det gøres ved at sætte dem til at bruge hver sin port. Når registreringsservicen startes, angives portnummeret som parameter. De programmer, der skal bruge registreringsservicen, skal angive portnummeret efter navnet på serveren. For eksempel vil //void-main:1234/Math angive, at registreringsservices på port 1234 på computeren void-main skal bruges. Angives intet portnummer bruges port 1099.

Kan RMI bruges fra JDK 1.0.2?

 

RMI kan godt bruges fra JDK 1.0.2, men den er ikke en integreret del. Det vil sige, at man er nødt til at downloade yderligere klassefiler fra Javasofts hjemmeside. Man skal være opmærksom på, at visse funktionskald har andre navne, end de, der er gennemgået her i bogen.


Hvorfor får jeg fejlen ”java.lang.ClassMismatchError”, når jeg prøver at køre mit program?

 

Sandsynligvis er en eller flere af klassefilerne blevet ændret. For at ændringen kan træde i kraft er det nødvendigt at genstarte alle programmer, der stiller RMI-objekter til rådighed for klienter. Også selve registreringsservices skal genstartes.

 

Er det muligt at se, om et objekt stadig bliver brugt af klienter?

 

Ja. Ved at lade objektet implementere interfacet java.rmi.server.Unreferenced får objektet signaturen til en metoden void unreferenced(), der automatisk bliver kaldt, når der ikke længere er flere referencer til objektet. Hvis objektet er bundet i registreringsservices, vil denne funktion aldrig blive udført, fordi registreringsservicen i så fald altid vil have en reference til den.

Er det fra serveren muligt at se, hvilken computer klientprogrammet kører på?

 

Ja. Det kan lade sig gøre ved hjælp af funktionen java.rmi.server.RemoteServer.getClientHost(). Funktionen er defineret som

 

public static String getClientHost() throws ServerNotActiveException

 

ServerNotActiveException opstår, hvis funktionen bliver kaldt i en funktion, der ikke er en del af RMI.

 

Du læser en gammel bog
Den bog, du læser her, er fra 1998, og mange ting kan have ændret sig siden da.
Vi håber, at du stadig kan finde relevant information i den.
Hvis du vil læse aktuelle oplysninger om de avancerede dele af Java, anbefaler vi
bogen Core Java – Advanced Features

Comments are closed.