inhoudstafel en auteursrecht
* STER *


7. Gedistribueerd programmeren

7.1 Definitie

Een computerprogramma heet gedistribueerd als bij de normale werking instructies worden uitgevoerd op minstens twee verschillende machines, en gegevens moeten worden uitgewisseld tussen verschillende machines.

Bij Java-programma's kunnen we dit begrip nog uitbreiden door de term "machine" te lezen als "virtuele machine". In dat geval kan je zelfs een gedistribueerd programma bouwen en gebruiken op één geïsoleerde computer! Vanuit het standpunt van de programmeur gaat het hier echter over twee verschillende machines, zodat een speciale inspanning nodig is voor de onderlinge communicatie.

Een client/serversysteem is een bijzonder soort gedistribueerd informatiesysteem waarbij aan één programma of object een dienstverlenende rol wordt toegekend, terwijl andere programma's of objecten van die diensten gebruikmaken. Bij een peer-to-peersysteem vervullen de verschillende componenten gelijke rollen.

Het bekendste voorbeeld van een client/servertoepassing is het World Wide Web. De belangrijkste programma-componenten zijn enerzijds de browser, anderzijds de web-serversoftware.

In een bedrijfscontext zal het servergedeelte heel vaak bestaan uit een relationeel gegevensbeheerssysteem.

7.2 Wanneer is gedistribueerd programmeren aangewezen ?

Kort antwoord: NOOIT, of toch zo weinig mogelijk. Gedistribueerd programmeren schept veel meer uitdagingen dan mogelijkheden. Een gedistribueerd programma is moeilijker te ontwerpen, bouwen, gebruiken en onderhouden dan een alleenstaand programma.

Natuurlijk zijn er enkele situaties waarin gedistribueerd programmeren een noodzakelijk kwaad vormt, anders zou niemand eraan beginnen. De twee meest voorkomende redenen waarom een informatiesysteem over verschillende machines verdeeld wordt, zijn:

  1. Geografische spreiding van input(s) en output(s). Vaak worden gegevens op één plaats gecentraliseerd opgeslagen en beheerd, terwijl veel verspreide personen de gegevens moeten kunnen raadplegen. Het Web is hiervan een extreem voorbeeld, waarbij iedereen beheerdertje kan spelen van zijn/haar eigen website.
  2. Verdeling van de werklast. Sommige taken zijn zo zwaar dat één computer ze moeilijk tijdig kan vervullen: dan kan het werk verdeeld worden tussen verschillende machines, hetzij identieke kopieën, hetzij computers die elk een gespecialiseerd onderdeel afhandelen.

7.3 Architectuur van een gedistribueerd informatiesysteem

De belangrijkste beslissing bij het ontwerp van een gedistribueerd informatiesysteem is precies de verdeling van de taken en gegevens over de verschillende componenten. Daarbij kan rekening worden gehouden met de volgende criteria (min of meer in dalende volgorde van belang):

Een typisch informatiesysteem heeft intern globaal een drielagige architectuur: opslag van gegevens, business logic, presentatie van gegevens.

De eenvoudigste client/serversystemen bestaan uit twee componenten (jawel: de client en de server!) Traditioneel worden vijf types onderscheiden, naargelang van de plaats waar de scheiding tussen client en server wordt getrokken:

Momenteel is er veel te doen rond drielagige systemen ("three-tier client/server systems"). Daarbij zijn drie machines betrokken, en de scheiding tussen lagen komt netjes overeen met de scheiding tussen machines.

7.4 Infrastructuur

Infrastructuur is de technologie die communicatie tussen programma-componenten toelaat. De belangrijkste infrastructuur-beslissingen waar de ontwerper van een gedistribueerd systeem mee geconfronteerd wordt, zijn: enerzijds de keuze van de communicatietechnologie, anderzijds de keuze van de middleware.

Communicatietechnologie is een combinatie van systemen en protocollen. Traditioneel worden communicatie-technieken onderverdeeld in (zeven of minder) functionele lagen, zoals bijvoorbeeld in het OSI-model. De alomtegenwoordigheid van Internet maakt dat Java speciale aandacht besteedt aan de protocollen uit die context, vooral het netwerkprotocol IP (Internét Protocol) en de transportprotocollen TCP (Transfer Control Protocol) en UDP (Unreliable Datagram Protocol). Voor deze tekst volstaat het te weten dat TCP communicatie via een verbinding tot stand brengt, en dus geschikt is voor dialogen, terwijl UDP verbindingsloos werkt, en zich dus leent voor afzonderlijke boodschappen.

Middleware is de software die de communicatie-aspecten van het systeem voor zich neemt. Dankzij de middleware krijgen de verschillende programma-componenten van het systeem de illusie dat ze zelfstandig draaiende programma's zijn. Men onderscheidt vijf grote soorten middleware:

De rest van dit hoofdstuk gaat over twee bijzondere soorten middleware die de Java-programmeur standaard ter beschikking staan: Remote Method Invocation en Java IDL. De eerste soort implementeert een object-georiënteerde versie van verre procedure-aanroep die geschikt is voor situaties waarin alle componenten van het gedistribueerde systeem in Java geschreven worden. Java IDL daarentegen geeft toegang tot een taal- en platform-onafhankelijke object request broker zoals gedefinieerd in de CORBA-standaard van OMG.

7.5 Java IDL

De CORBA-standaard hanteert de begrippen "client" en "server" in een heel algemene zin, die ook buiten client/serversystemen van toepassing is. Voor de rest van deze paragraaf hanteren we de volgende definities.

Server: een object dat aan de buitenwereld diensten aanbiedt via methoden en/of attributen;

Client: eender welke reeks programma-instructies (al dan niet object-georiënteerd) waarin methoden en/of attributen van servers gebruikt worden.

Het mag duidelijk zijn dat eenzelfde Java-object nu eens de rol van client, dan weer de rol van server kan aannemen. Daarbij hoeven circulaire relaties geen probleem te zijn, bijvoorbeeld twee verschillende objecten kunnen elkaars methoden aanroepen.

Een CORBA-server formuleert zijn uitzicht voor de buitenwereld aan de hand van een interface. Zo'n CORBA-interface lijkt veel op een interface in de programmeertaal Java, met enkele subtiele verschillen. De belangrijkste gelijkenis is dat een interface methoden kan bevatten, elk met een reeks van nul of meer parameters en een eventueel terugkeertype. CORBA-interfaces worden geformuleerd in een aparte, eenvoudig computertaal, de zogenaamde Interface Definition Language (IDL).

IDL is géén complete programmeertaal. Ze bevat bijvoorbeeld geen if-opdracht, geen lussen, enzovoort. IDL definieert uitsluitend object- en gegevenstypes.

Elke afzonderlijke programmeertaal waarin CORBA-componenten geïmplementeerd worden, dient dan te definiëren hoe de IDL-datastructuren naar de eigen grammatica worden vertaald. Een dergelijke language mapping bestaat reeds voor ondermeer de programmeertalen C, C++, Smalltalk, Ada, COBOL en natuurlijk ook Java.

De belangrijkste elementen van een IDL-bestand zijn de eigenlijke interfaces, ingeleid door het sleutelwoord interface. Een interface bestaat uit een opsomming, tussen een paar accoladen en telkens gevolgd door een kommapunt, van nul of meer van de volgende elementen:

In deze tekst beperken we ons tot interfaces die uitsluitend uit operaties bestaan. Operaties zijn ongeveer hetzelfde als de (abstracte) Java-methoden in een Java-interfacedeclaratie, met de volgende verschillen:

Voorbeeld

interface Vertaler {
  string vertaal(in string tekst);
};

De volgende tabel beschrijft de correspondentie tussen sommige elementaire IDL-types (links) en hun Java-tegenhanger (rechts).

CORBA-type minimale lengte volgens CORBA Java-type precieze lengte in de JVM
short 2 bytes short 2 bytes
long 4 bytes int 4 bytes
long long 8 bytes long 8 bytes
char 8 bits char 16 bits
string - String -
boolean - boolean -
octet 8 bits byte 8 bits
float 4 bytes float 4 bytes
double 8 bytes double 8 bytes

Een CORBA-client kan de methoden van een server-object aanroepen, maar moet daartoe eerst wel een referentie naar het object bekomen, een soort identiteit van het object. Het object kan bijvoorbeeld zijn meegedeeld als terugkeerwaarde van een vroegere methode-aanroep.

Natuurlijk is er hier een kip-eiprobleem: vóór de eerste methode-aanroep moet het clientprogramma een allereerste objectreferentie kunnen bekomen van een object dat zich op de andere machine bevat. Hiertoe zullen we de volgende twee mechanismen bespreken:

CORBA is erg beperkt in de mogelijke types van parameters en terugkeerwaarden. Ofwel behoren deze tot een vaste standaardset van types (voornamelijk tekst en getallen): in dat geval wordt een kopie van de gegeven waarde van de ene machine naar de andere overgebracht. Ofwel zijn het CORBA-objecttypes: in dat geval wordt een objectreferentie van de ene machine naar de andere overgebracht.

Hello, Corba

We beschrijven nu een elementair voorbeeld van een gedistribueerde toepassing. De server bestaat uit een object met één methode die een vaste tekst teruggeeft. De client roept die methode aan en toont de ontvangen tekst op het scherm.

CORBA is in principe taal-onafhankelijk, dus de verschillende componenten kunnen in verschillende talen geschreven worden. In deze tekst zullen echter alle voorbeelden Java-programma's zijn, zowel aan de kant van de server als aan de client-zijde.

// bestand HelloCorba.idl:

interface HelloCorba {
  string getMessage();
};

De Java Development Kit bevat een eigen IDL-compiler, idlj, die definities van IDL-interfaces omzet naar corresponderende Java-klassen en -interfaces. Voer de vertaling van bovenstaande interface uit met het commando

idlj -fall HelloCorba.idl

De IDL-naar-Java-compiler genereert nu automatisch niet minder dan zes Java-bestanden:

_HelloCorbaImplBase.java
_HelloCorbaStub.java
HelloCorba.java
HelloCorbaHelper.java
HelloCorbaHolder.java
HelloCorbaOperations.java

Het bestand HelloCorba.java definieert de interface HelloCorba waarvan we een implementatie gaan definiëren. In feite is dit een lege doos:

public interface HelloCorba
  extends HelloCorbaOperations,
  org.omg.CORBA.Object,
  org.omg.CORBA.portable.IDLEntity 
{
}

De eigenlijke methode getMessage() wordt namelijk gedeclareerd in de superinterface HelloCorbaOperations, ondergebracht in het bestand HelloCorbaOperations.java.

public interface HelloCorbaOperations 
{
  String getMessage ();
}

Voor de implementatie van deze interface moeten we natuurlijk zelf één of meer objectklassen definiëren. Dat wordt ons makkelijk gemaakt door de abstracte klasse _HelloCorbaImplBase in het bestand _HelloCorbaImplBase.java, die reeds alle technische verplichtingen van CORBA-objecten op zich neemt. Wij moeten uitsluitend een subklassen daarvan definiëren, die de abstracte methode getMessage() concreet implementeert. Creëer dus een bestand HelloCorbaObject.java met de volgende inhoud:

import org.omg.CORBA.*;
import org.omg.CosNaming.*;

public class HelloCorbaObject extends _HelloCorbaImplBase {
  public String getMessage() {
    return "Hello, CORBA !";
  }

  public static void main(String[] args) {
    /* 1. Construeer een server-object */
    HelloCorbaObject o = new HelloCorbaObject();

    /* 2. Verkrijg een referentie naar de
          Object Request Broker */
    ORB orb = ORB.init(new String[0], null);

    /* 3. Registreer het server-object bij
          de Object Request Broker */
    orb.connect(o);

    try {
      /* 4. Verkrijg een referentie naar de
            Naming Service */
      org.omg.CORBA.Object objRef =
        orb.resolve_initial_references("NameService");
      NamingContext ncRef =
        NamingContextHelper.narrow(objRef);

      /* 5. Registreer het server-object bij
            de Naming Service onder de naam
            "Hello" */
      NameComponent nc = new NameComponent("Hello", "");
      NameComponent[] path = { nc };
      ncRef.rebind(path, o);

      /* 6. Wacht eeuwig op binnenkomende verzoeken */
      java.lang.Object sync = new java.lang.Object();
      System.out.println("Klaar om verzoekjes te ontvangen...");
      synchronized(sync) {
        sync.wait();
      }
    }
    catch (org.omg.CORBA.ORBPackage.InvalidName exc) {
      System.err.println("HelloCorbaObject:"
        + " Name Service niet gevonden.");
    }
    catch (org.omg.CosNaming.NamingContextPackage.NotFound exc) {
      System.err.println("HelloCorbaObject:"
        + " Kon object niet registreren bij Name Service.");
      System.err.println(exc);
    }
    catch (org.omg.CosNaming.NamingContextPackage.CannotProceed exc) {
      System.err.println("HelloCorbaObject:"
        + " Kon object niet registreren bij Name Service.");
      System.err.println(exc);
    }
    catch (org.omg.CosNaming.NamingContextPackage.InvalidName exc) {
      System.err.println("HelloCorbaObject:"
        + " Kon object niet registreren bij Name Service.");
      System.err.println(exc);
    }
    catch (InterruptedException exc) {
      System.err.println("HelloCorbaObject:"
        + " Onderbroken door extern signaal.");
    }
  }
}

In de methode main wordt het server-object geconstrueerd en geregistreerd, achtereenvolgens bij de ORB en bij de Naming Service (met de methode rebind). Aan de hand van de Naming Service kunnen clients achteraf een referentie naar het server-object bekomen.

Het client-programma kan er dan bijvoorbeeld als volgt uitzien.

import org.omg.CosNaming.*;
import org.omg.CORBA.*;

public class HelloCorbaClient {
  public static void main (String[] args) throws Exception {
    /* 1. Verkrijg een referentie naar de
          Object Request Broker */
    ORB orb = ORB.init(new String[0], null);

    /* 2. Verkrijg een referentie naar de
          Naming Service */
    org.omg.CORBA.Object objRef =
      orb.resolve_initial_references("NameService");
    NamingContext ncRef =
      NamingContextHelper.narrow(objRef);

    /* 3. Gebruik de Naming Service om een
          referentie te verkrijgen naar een
          HelloCorba-object dat geregistreerd
          is onder de naam "Hello" */
    NameComponent nc = new NameComponent("Hello", "");
    NameComponent path[] = { nc };
    HelloCorba helloRef = HelloCorbaHelper.narrow(ncRef.resolve(path));

    /* 4. Laat het HelloCorba-object zijn ding doen */
    String message = helloRef.getMessage();
    System.out.println(message);
  }
}

We zullen bovenstaande programma's op eenzelfde machine in twee verschillende virtuele machines laten draaien. Bovendien moet de Name Service afzonderlijk worden opgestart. Open dus bijvoorbeeld op een Windows-computer drie DOS-vensters.

Start eerst de Transient Name Service op met het commando

tnameserve
Initial Naming Context: IOR:(lange reeks cijfers en letters) TransientNameServer: setting port for initial object references to: 900 Ready.

Start vervolgens de main-methode van het serverprogramma (als een afzonderlijk programma):

java HelloCorbaObject
Klaar om verzoekjes te ontvangen...

Vervolgens kan je als derde programma de client een of meer keren starten:

java HelloCorbaClient
Hello, CORBA !

Verstrenging

In het bovenstaande "Hello, CORBA !" voorbeeld bekwam de client een referentie naar het verafgelegen server-object door bemiddeling van de Naming Service. CORBA-objectreferenties kunnen ook rechtstreeks worden bekomen door ze om te zetten naar het tussenformaat van een tekststring. Dit proces heet verstrenging (Eng. stringification). Het maakt gebruik van de volgende twee methoden van de klasse ORB:

public abstract String object_to_string(org.omg.CORBA.Object obj)
public abstract org.omg.CORBA.Object string_to_object(String str)

We geven hieronder de aangepaste versies van de server- en client-code voor het "Hello, CORBA !" voorbeeld. De werking van het programma is overigens hetzelfde, met dien verstande dat beide programma's wel op een of andere manier toegang moeten hebben tot de locatie van het tekstbestand myfile.txt.

import org.omg.CORBA.*;
import org.omg.CosNaming.*;
import java.io.*;

public class HelloCorbaObjectStringified extends _HelloCorbaImplBase {
  public String getMessage() {
    return "Hello, CORBA !";
  }

  public static void main(String[] args) {
    /* 1. Construeer een server-object */
    HelloCorbaObjectStringified o = new HelloCorbaObjectStringified();

    /* 2. Verkrijg een referentie naar de
          Object Request Broker */
    ORB orb = ORB.init(new String[0], null);

    /* 3. Registreer het server-object bij
          de Object Request Broker */
    orb.connect(o);

    try {
      /* 4. Schrijf een verstrengde versie
            van de objectreferentie naar het
            tekstbestand "myfile.txt" */
      PrintWriter outFile = new PrintWriter(
        new FileWriter("myfile.txt"), true);
      outFile.println(orb.object_to_string(o));

      /* 5. Wacht eeuwig op binnenkomende verzoeken */
      java.lang.Object sync = new java.lang.Object();
      System.out.println("Klaar om verzoekjes te ontvangen...");
      synchronized(sync) {
        sync.wait();
      }
    }
    catch (IOException exc) {
      System.err.println("HelloCorbaObjectStringified: "
        + " Kon geen verstrengde objectreferentie schrijven"
        + " naar bestand myfile.txt");
    }
    catch (InterruptedException exc) {
      System.err.println("HelloCorbaObject:"
        + " Onderbroken door extern signaal.");
    }
  }
}

import org.omg.CosNaming.*;
import org.omg.CORBA.*;
import java.io.*;

public class HelloCorbaClientStringified {
  public static void main (String[] args) throws Exception {
    /* 1. Verkrijg een referentie naar de
          Object Request Broker */
    ORB orb = ORB.init(new String[0], null);

    try {
      /* 2. Gebruik "ontstrenging"
            referentie te verkrijgen naar een
            HelloCorba-object  */
      BufferedReader inFile = new BufferedReader(
        new FileReader("myfile.txt"));
      HelloCorba helloRef = HelloCorbaHelper.narrow(
        orb.string_to_object(inFile.readLine()));

      /* 3. Laat het HelloCorba-object zijn ding doen */
      String message = helloRef.getMessage();
      System.out.println(message);
    }
    catch (IOException exc) {
      System.err.println("HelloCorbaObjectStringified: "
        + " Kon geen verstrengde objectreferentie lezen"
        + " uit bestand myfile.txt");
    }
  }
}

Tijdens en na het server-programma bevat het bestand myfile.txt de volgende tekstregel. We splitsen hem over verschillende regels omwille van de zichtbaarheid is de browser, maar in werkelijkheid betreft het één enkele regel.

IOR:000000000000001349444c3a48656c6c6f43
6f7262613a312e30000000000001000000000000
0054000101000000000e3231332e3232342e3132
2e323600049800000018afabcafe00000002f1de
e766000000080000000000000000000000010000
0001000000140000000000010020000000000001
010000000000

Doorgeven van parameters

In de meeste gedistribueerde toepassingen zal informatie worden verstuurd tussen clients en servers in beide richtingen. In IDL kan dit zowel door de parameters en terugkeerwaarde van een operatie, als door een object-attribuut. Zowel operaties als attributen worden echter in Java weergegeven door methoden.

Beschouwen we als voorbeeld een server-object dat in staat is de rekenkundige bewerkingen optellen en aftrekken van gehele getallen uit te voeren. Dit is een karikatuur van de meer realistische situatie waarbij intensief rekenwerk door één computer aan een andere wordt gedelegeerd.

De IDL-interface is als volgt gedefinieerd.

module rekenen {
  interface EenvoudigeRekenaar {
    long telOp(in long term1, in long term2);
    long trekAf(in long term1, in long term2);
  };
};

Modules zijn het middel om interfaces in een hiërarchische structuur te ordenen, ongeveer zoals pakketten in Java. Het belangrijkste verschil is dat een module ook andere modules kan bevatten, terwijl een Java-pakket nooit een ander Java-pakket bevat.

Bij de vertaling van IDL-modules in Java-pakketten wordt de hiërarchische structuur weergegeven door de puntjesnotatie. Zo zal een module datums die deel uitmaakt van een grotere module astronomie door de IDL-compiler in een pakket met de samengestelde naam datums.astronomie vertaald worden.

De interface EenvoudigeRekenaar beschrijft het uiterlijke gedrag van objecten die twee diensten aanbieden: het optellen resp. aftrekken van twee gehele getallen, met als terugkeerwaarde het resultaat van de berekening. We herinneren eraan dat het CORBA-type long overeenstemt met het Java-type int, terwijl het CORBA-type long long overeenstemt met het Java-type long.

De volgende code definieert een klasse EenvoudigeRekenServer die bovenstaande IDL-interface realiseert. Ze bevat bovendien een main-methode om één dergelijk object te construeren en bij de ORB en de Naming Service de registreren.

import rekenen.*;
import org.omg.CORBA.*;
import org.omg.CosNaming.*;
import org.omg.CosNaming.NamingContextPackage.*;

public class EenvoudigeRekenServer
  extends _EenvoudigeRekenaarImplBase {
  public int telOp(int term1, int term2) {
    return term1 + term2;
  }

  public int trekAf(int term1, int term2) {
    return term1 - term2;
  }

  public static void main(String[] args) throws Exception {
    EenvoudigeRekenaar rek = new EenvoudigeRekenServer();
    ORB orb = ORB.init(new String[0], null);
    orb.connect(rek);
    org.omg.CORBA.Object objRef =
      orb.resolve_initial_references("NameService");
    NamingContext ncRef =
      NamingContextHelper.narrow(objRef);
    NameComponent nc = new NameComponent("DeRekenaar", "");
    NameComponent[] path = { nc };
    ncRef.rebind(path, rek);
    java.lang.Object sync = new java.lang.Object();
    synchronized(sync) {
      sync.wait();
    }
  }
}

Aan de client-kant schrijven we twee korte Javaprogramma's, TelOp en TrekAf, die telkens het volgende doen:

We geven hier slechts de broncode voor de aftrekking, aangezien die van de optelling nagenoeg identiek is.

import rekenen.*;
import org.omg.CORBA.*;
import org.omg.CosNaming.*;
import org.omg.CosNaming.NamingContextPackage.*;

public class TrekAf {
  public static void main(String[] args) {
    try {
      int getal1 = Integer.parseInt(args[0]);
      int getal2 = Integer.parseInt(args[1]);

      ORB orb = ORB.init(new String[0], null);
      org.omg.CORBA.Object objRef =
        orb.resolve_initial_references("NameService");
      NamingContext ncRef =
        NamingContextHelper.narrow(objRef);
      NameComponent nc = new NameComponent("DeRekenaar", "");
      NameComponent[] path = { nc };
      EenvoudigeRekenaar rek =
        EenvoudigeRekenaarHelper.narrow(ncRef.resolve(path));

      int resultaat = rek.trekAf(getal1, getal2);

      System.out.println(getal1 + " - " + getal2
        + " = " + resultaat);
    }
    catch (ArrayIndexOutOfBoundsException e) {
      System.err.println("Gelieve twee argumenten mee te geven");
    }
    catch (NumberFormatException e) {
      System.err.println("De argumenten moeten gehele getallen zijn");
    }
    catch (Exception e) {
      System.err.println(
        "Algemene fout bij het contacteren van de rekenserver");
    }
  }
}

Datastructuren

IDL laat toe gestructureerde gegevenstypes op te bouwen die zijn samengesteld uit bestaande types. Daarbij kan de programmeur gebruik maken van elementaire types, zoals char en long, maar ook van andere gestructureerde types. We kunnen dus een complete hiërarchie van gegevenstypes opbouwen.

Gestructureerde gegevenstypes worden opgebouwd met het sleutelwoord struct. Stel dat we bijvoorbeeld een gegevenstype Klant willen opbouwen aan de hand van een identificatienummer, een naam en een woonplaats. In IDL ziet dat er als volgt uit.

struct Klant {
  long nummer;
  string naam;
  string woonplaats;
};

Deze nieuwe, gestructureerde gegevenstypes kunnen vervolgens in verdere definities (bijvoorbeeld interfaces) gebruikt worden als type van attributen, parameters en terugkeerwaarden.

Beschouw de volgende IDL-module. Ze definieert de uitwendige interface van een printerwachtlijn: toevoegen van nieuwe printerjobs, of uitvoeren van de printerjob die zich het langst in de wachtlijn bevindt (first-in-first-out). Een printerjob wordt gekenmerkt door een naam, een hoofding en een tekst (die moet worden afgedrukt).

module afdrukken {
  struct PrinterJob {
    string naam;
    string hoofding;
    string tekst;
  };
  interface Wachtlijn {
    void voegToe(in PrinterJob p);
    void drukVolgende();
  };
};

De parameter van de operatie voegToe in van het gestructureerde type PrinterJob. Als een client de methode voegToe van een server-object wil aanroepen, moet ze de vereiste datastructuur als parameter meegeven. Er zullen dan effectief drie tekststrings langs het netwerk worden getransporteerd.

We zouden bovenstaande situatie ook kunnen uitwerken door een PrinterJob als een volwaardig CORBA-object te beschouwen:

module afdrukken {
  interface PrinterJob {
    string naam;
    string hoofding;
    string tekst;
  };
  interface Wachtlijn {
    void voegToe(in PrinterJob p);
    void drukVolgende();
  };
};

Maar deze aanpak heeft twee belangrijke gevolgen:

  1. CORBA-objecten worden nooit van de ene machine naar de andere getransporteerd. Bij de aanroep van de operatie voegToe zal in de tweede aanpak slechts een referentie naar een object worden getransporteerd: de drie strings bevinden zich alleen in het geheugen van de machine waar het PrintJob-object gecreëerd is.
  2. Bij een CORBA-interface moet nog een afzonderlijke server-implementatie worden geprogrammeerd. De client moet op een of andere manier een referentie naar dat object bekomen. Een struct is volledig gedefinieerd in de interface, en kan dus door alle deelnemers (client en server) worden gecreëerd en integraal over het netwerk getransporteerd worden.

We werken nu een voorbeeld uit van (een lichte variatie op) de tweede aanpak. Bij het gebruiken van het voorbeeld zal aanschouwelijk bewezen worden dat de PrinterJob uitsluitend leeft in de virtuele machine waarin ze gecreëerd wordt.

Het IDL-bestand ziet er als volgt uit.

module afdrukken {
  interface PrintJob {
    void wissen();
    void toevoegen(in string tekst);
    void afdrukken();
  };
  interface Printer {
    void plaatsInWachtlijn(in PrintJob p);
    void voerVolgendeJobUit();
    PrintJob maakNieuweJob();
  };
};

We implementeren de PrintJob-interface met de klasse PrintJobServer:

import afdrukken.*;
import org.omg.CORBA.*;
import org.omg.CosNaming.*;
import org.omg.CosNaming.NamingContextPackage.*;

public class PrintJobServer
  extends _PrintJobImplBase {
  private StringBuffer sb = new StringBuffer("");
  public void wissen() {
    sb = new StringBuffer("");
  }
  public void toevoegen(String tekst) {
    sb.append(tekst);
  }
  public void afdrukken() {
    System.out.println(sb);
  }
}

De klasse Lijndrukker implementeert de CORBA-interface Printer. Haar main-methode creëert tevens een serverwachtlijn waarvan de naam gegeven wordt door het eerste argument van de opdrachtregel.

import afdrukken.*;
import org.omg.CORBA.*;
import org.omg.CosNaming.*;
import org.omg.CosNaming.NamingContextPackage.*;
import java.util.*;

public class Lijndrukker
  extends _PrinterImplBase {
  public final static int CAPACITEIT = 10;
  private LinkedList wachtlijn = new LinkedList();
  public void plaatsInWachtlijn(PrintJob p) {
    wachtlijn.addLast(p);
  }
  public void voerVolgendeJobUit() {
    java.lang.Object o = wachtlijn.removeFirst();
    if (o instanceof PrintJob) {
      PrintJob p = (PrintJob) o;
      p.afdrukken();
    }
  }
  public PrintJob maakNieuweJob() {
    return new PrintJobServer();
  }
  public static void main(String[] args) {
    try {
      Lijndrukker ld = new Lijndrukker();
      ORB orb = ORB.init(new String[0], null);
      orb.connect(ld);
      org.omg.CORBA.Object objRef =
        orb.resolve_initial_references("NameService");
      NamingContext ncRef =
        NamingContextHelper.narrow(objRef);
      NameComponent nc = new NameComponent(args[0], "Printer");
      NameComponent[] path = { nc };
      ncRef.rebind(path, ld);
      java.lang.Object sync = new java.lang.Object();
      synchronized(sync) {
        sync.wait();
      }
    }
    catch (org.omg.CORBA.ORBPackage.InvalidName e) {
      System.err.println("Kan de Naming Service niet contacteren.");
    }
    catch (InvalidName e) {
      System.err.println("Ongeldige naam.");
    }
    catch (NotFound e) {
      System.err.println("Kan de Lijndrukker niet registreren"
        + " bij de Naming Service.");
    }
    catch (CannotProceed e) {
      System.err.println("Kan de Lijndrukker niet registreren"
        + " bij de Naming Service.");
    }
    catch (InterruptedException e) {
      System.err.println("Lijndrukkerservice onderbroken.");
    }
  }
}

Dankzij de methode maakNieuweJob fungeert een object van de klasse Lijndrukker als een object factory. We hoeven dus niet iedere PrintJob te registreren bij de Naming Service.

Tenslotte hebben we een kort testprogramma nodig aan de kant van de client. Het volgende programma communiceert met evenveel servers als er opdrachtregel-parameters gegeven worden. Elke server krijgt de opdracht twee jobs uit te voeren. Er is evenwel een klein verschil tussen de twee job-objecten voor wat betreft hun creatie: het eerste object wordt gecreëerd op dezelfde server als waarop het wordt uitgevoerd; het tweede object wordt steeds op de eerste server gecreëerd.

import afdrukken.*;
import org.omg.CORBA.*;
import org.omg.CosNaming.*;
import org.omg.CosNaming.NamingContextPackage.*;

public class AfdrukTester {
  public static void main(String[] args) throws Exception {
    ORB orb = ORB.init(new String[0], null);
    org.omg.CORBA.Object objRef =
      orb.resolve_initial_references("NameService");
    NamingContext ncRef =
      NamingContextHelper.narrow(objRef);

    int aantal = args.length;
    Printer[] p = new Printer[aantal];
    for (int i = 0; i < aantal; i++) {
      NameComponent nc = new NameComponent(args[i], "Printer");
      NameComponent[] path = { nc };
      p[i] = PrinterHelper.narrow(ncRef.resolve(path));
      PrintJob pj = p[i].maakNieuweJob();
      pj.toevoegen("Ik ben de " + (i+1) + "e printer"
        + " en mijn naam is " + args[i]);
      p[i].plaatsInWachtlijn(pj);
      pj = p[0].maakNieuweJob();
      pj.toevoegen("Deze job is door de 1e printer"
        + " aangemaakt, en vervolgens naar de " + (i+1) + "e printer"
        + " ter uitvoering doorgestuurd.");
      p[i].plaatsInWachtlijn(pj);
    }
    for (int i = 0; i < aantal; i++) {
      p[i].voerVolgendeJobUit();
      p[i].voerVolgendeJobUit();
    }
  }
}

We zullen dit testprogramma nu uitvoeren met twee printservers, nummer1 en nummer2 genaamd. Open dus vier DOS-vensters:

Kijk nu naar het tweede en het derde venster. Het blijkt dat de printjobs niet worden uitgevoerd op de machine die de methode afdrukken() aanroept, maar wel op de machine waar de objecten gecreëerd zijn! De terugkeerwaarde van maakNieuweJob() en de parameter van plaatsInWachtlijn zijn slechts verwijzingen naar jobs. De eigenlijke string-data worden niet tussen virtuele machines onderling doorgegeven.

nummer1 verwerkt twee printjobs, nummer2 geen enkele

Overerving van interfaces

De CORBA Interface Definition Language, in al haar beperktheid als declaratieve computertaal, is toch in hoge mate object-georiënteerd. In het bijzonder kent zij ook de mogelijkheid, interfaces van elkaar te laten erven. En net zoals in Java kan één interface erven van verschillende andere interfaces (multiple inheritance).

Syntactisch worden de moederinterfaces van een interface opgesomd, gescheiden door komma's, na een dubbelepunt die volgt op de naam van de kind-interface.

Stel dat we de interface EenvoudigeRekenaar (zie paragraaf "doorgeven van parameters" hierboven) willen uitbreiden door de rekenmachine twee extra bewerkingen te geven: vermenigvuldigen en delen. We doen dit best niet door aan EenvoudigeRekenaar twee nieuwe abstracte methoden toe te voegen. Interfaces can't grow. Dan zouden namelijk alle bestaande servers ongeldige implementaties vormen: ze plegen contractbreuk doordat ze de nieuwe methoden niet implementeren.

In plaats daarvan definiëren we een nieuwe interface die een uitbreiding is van de bestaande.

module rekenen {
  interface EenvoudigeRekenaar {
    long telOp(in long term1, in long term2);
    long trekAf(in long term1, in long term2);
  };
  interface ComplexeRekenaar : EenvoudigeRekenaar {
    long long vermenigvuldig(in long term1, in long term2);
    long deel(in long term1, in long term2);
  };
};

De interface ComplexeRekenaar telt vier abstracte methoden. Naast de twee uitdrukkelijk vermelde, erft hij ook de bestaande methoden van EenvoudigeRekenaar

Tot dusver is er niet veel aan de hand. We kunnen gewoon een serverimplementatie maken, ComplexeRekenServer, die allevier de methoden van ComplexeRekenaar realiseert.

Maar wat als we de implementatie van ComplexeRekenServer willen laten erven van de bestaande serverimplementatie EenvoudigeRekenServer ? We zijn tenslotte object-georiënteerd aan het programmeren, dus we willen codeduplicatie vermijden!

Het probleem is: de klasse ComplexeRekenServer kan niet afgeleid worden van de klasse EenvoudigeRekenServer, omdat ComplexeRekenServer reeds moet afgeleid worden van de (automatisch gegenereerde) klasse _ComplexeRekenaarImplBase. En Java-klassen kennen geen meervoudige overerving.

public class ComplexeRekenServer
  extends _ComplexeRekenaarImplBase { // en de andere klasse ???
...
}

We zullen dit oplossen door delegatie. Dat betekent dat een ComplexeRekenServer geen EenvoudigeRekenServer is, maar er wél een bevat.

Hier is de volledige implementatie van de klasse ComplexeRekenServer, inclusief een main-methode om het serverobject de construeren en te registeren. De client-code laten we als een oefening voor de lezer.

import rekenen.*;
import org.omg.CORBA.*;
import org.omg.CosNaming.*;
import org.omg.CosNaming.NamingContextPackage.*;

public class ComplexeRekenServer
  extends _ComplexeRekenaarImplBase {

  /** Inwendige referentie naar een object waaraan we
      het rekenwerk voor de bewerkingen telOp en trekAf
      kunnen toevertrouwen. Dit hoeft geen CORBA-object
      te zijn: een Java-object volstaat.
  */
  private EenvoudigeRekenServer gedelegeerde = new EenvoudigeRekenServer();

  public int telOp(int term1, int term2) {
    // niet uitrekenen, maar delegeren
    return gedelegeerde.telOp(term1, term2);
  }

  public int trekAf(int term1, int term2) {
    // niet uitrekenen, maar delegeren
    return gedelegeerde.trekAf(term1, term2);
  }

  public long vermenigvuldig(int factor1, int factor2) {
    // dit moeten we zelf doen
    return ((long) factor1) * ((long) factor2);
  }

  public int deel(int deeltal, int deler) {
    // dit moeten we zelf doen
    return deeltal / deler;
  }

  public static void main(String[] args) throws Exception {
    ComplexeRekenaar rek = new ComplexeRekenServer();
    ORB orb = ORB.init(new String[0], null);
    orb.connect(rek);
    org.omg.CORBA.Object objRef =
      orb.resolve_initial_references("NameService");
    NamingContext ncRef =
      NamingContextHelper.narrow(objRef);
    NameComponent nc = new NameComponent("NogEenRekenaar", "");
    NameComponent[] path = { nc };
    ncRef.rebind(path, rek);
    java.lang.Object sync = new java.lang.Object();
    synchronized(sync) {
      sync.wait();
    }
  }
}

7.6 Remote Method Invocation

In voorbereiding.


inhoudstafel en auteursrecht
* STER *

Valid HTML 4.0! Valid CSS!