MokaByte Numero 22 - Settembre 1998  


di
Lorenzo Bettini
  Java + IDL =
CORBA

Java IDL, l'ORB del jdk 1.2

  In questo articolo vedremo quello che la versione 1.2 del jdk (ancora in beta) mette a disposizione per l'implementazione di applicazioni che utilizzano l'architettura CORBA


Introduzione

CORBA (Common Object Request Broker Architecture) è lo standard per architetture ad oggetti distribuite sviluppato dal consorzio dell'Object Management Group (OMG). Si tratta di un'architettura per la realizzazione di un open software bus, appunto l'Object Request Broker (ORB). Utilizzando questo bus, applicazioni ad oggetti eterogenee possono interoperare attraverso la rete, indipendentemente dall'architettura e dai sistemi operativi; fin qui niente di nuovo: il nostro Java è nato per questo :-) In questo caso però l'indipendenza si espande al linguaggio: tali applicazioni (ed in particolare i componenti ad oggetti utilizzati da queste) possono essere scritti in linguaggi differenti. In questo modo un programma scritto in C++ può richiamare i metodi di un oggetto (magari remoto) scritto in Java, oppure in Smalltalk, o addirittura in COBOL!

Di CORBA (specialmente correlato a Java) è già stato parlato su MokaByte ([1], [2]), quindi ci si limiterà solo a riprendere gli aspetti più salienti di questa architettura, in modo da dare anche una piccola introduzione; per avere una visione più totale comunque si rimanda ai suddetti articoli.

In particolare in questo articolo si parlerà dell'ORB messo a disposizione (ovviamente gratuitamente) dalla Sun per il jdk 1.2: Java IDL; purtroppo nella versione beta 3, manca il compilatore necessario per utilizzare questo ORB, che dovrà essere scaricato manualmente dal sito della Sun all'indirizzo: http://developer.javasoft.com/developer/earlyAccess/jdk12/idltojava.html.

Vale la pena notare che funzionalità simili a quelle messe a disposizione da CORBA, erano ottenibili anche utilizzando le RMI (Remote Method Invocation) API (anche queste già trattate in MokaByte: [3], [4]), già presenti nel jdk; infatti in questo modo si possono richiamare metodi su oggetti remoti (una volta ottenuto un riferimento locale). La differenza sta nel fatto che l'RMI può essere applicato solo ad oggetti e componenti Java, mentre l'intento di CORBA è quello di far cooperare oggetti (remoti) scritti in linguaggi differenti. Un confronto fra CORBA e RMI si può trovare in [5].

CORBA

Diamo in questo paragrafo alcune nozioni fondamentali su CORBA. Per chi volesse approfondire, oltre agli articoli [1], [2], si consiglia di visitare il sito ufficiale della OMG: http://www.omg.org che contiene anche moltissima documentazione, da quella di base, alle vere e proprie specifiche CORBA, che dovrebbero essere implementate dai vari ORB per essere considerati CORBA compliant.

Ovviamente oggetti scritti in linguaggi differenti, in esecuzione su Sistemi Operativi differenti ed architetture completamente diverse non possono comunicare direttamente, ma devono utilizzare certe interfacce (e protocolli) e comunicare attraverso dei proxy che comunque rendono totalmente trasparenti i dettagli della comunicazione: una volta ottenuto un riferimento ad un oggetto remoto si potrà richiamare semplicemente un suo metodo nel modo usuale.

Questo del resto non è differente dall'RMI in cui si ha uno stub (dalla parte del client) ed uno skeleton (dalla parte del server) che fungono appunto da proxy. Anche in CORBA (o forse sarebbe meglio dire anche in RMI, come in CORBA, visto che quest'ultimo è nato molto prima di Java) i termini sono gli stessi. Nella figura seguente è infatti schematizzato (in modo molto semplificato) come avviene la chiamata di un metodo di un oggetto remoto:

Chiamata di un metodo di un oggetto remoto

Un servant è un'istanza dell'implementazione di un oggetto CORBA, mentre un server è un processo che istanzia i servant e li mette a disposizione dei client, che sono applicazioni che invocano metodi di oggetti CORBA. Un client di un oggetto CORBA richiama i metodi di un oggetto remoto tramite un object reference che ottiene dal server. Lo stub, tramite l'ORB, identifica la macchina su cui è presente il server CORBA che gestisce l'oggetto del quale si vuole richiamare il metodo. Una volta stabilita la connessione con tale macchina, la richiesta, insieme ad i vari eventuali parametri con cui si richiama il metodo, vengono spediti dall'ORB del client all'ORB del server; quest'ultimo ORB presenta la richiesta allo skeleton che provvede a richiamare il metodo del servant, e, una volta terminato, a consegnare i risultati al client (ovviamente sempre tramite l'ORB). Quindi, come già detto, tutte le comunicazioni sono trasparenti al client, che penserà di invocare semplicemente un metodo di un oggetto normale. I due ORB non devono essere necessariamente istanze della stessa implementazione: basta che implementino gli standard di CORBA; del resto questo è tipico dei protocolli di Internet: quando si accede ad un sito FTP, vi si accede indipendentemente dalla particolare implementazione del server, in quanto viene utilizzato un protocollo standard.

I dati prima di essere spediti vengono memorizzati in un formato opportuno standard di CORBA; tale processo viene detto marshaling. Il recupero dei dati sul server viene detto a sua volta unmarshaling.

Ovviamente, poiché tali oggetti possono essere implementati con linguaggi di programmazione differente, ci deve essere un modo tramite il quale la comunicazione possa avvenire; del resto per chiamare i metodi di un oggetto non se ne deve conoscere l'implementazione, ma solo conoscerne l'interfaccia (questo è vero nella normale programmazione, ed ancora di più nella programmazione ad oggetti che esalta il concetto di information hiding). Quindi è sufficiente avere a disposizione un modo standard per definire le interfacce degli oggetti CORBA.

Il metodo per definire tali interfacce è, tanto per cambiare, un linguaggio di programmazione, che fa parte dello standard CORBA: questo linguaggio si chiama IDL, ovverosia Interface Definition Language. Oltre all'ORB, quindi è necessario un compilatore per tale linguaggio; la Sun mette a disposizione per il jdk 1.2 il compilatore idltojava. Esistono in commercio molti ORB [6], con relativi compilatori IDL, ma questo ha il vantaggio di essere free! (esistono, a tal proposito anche altre implementazioni free). L'implementazione di questo ORB segue le specifiche 2.0 di CORBA.

A questo punto è bene fare alcuni chiarimenti: Java IDL è un ORB CORBA 2.0, e non, come potrebbe sembrare a prima vista, un'implementazione dell'IDL di OMG; ovviamente anche questo è incluso. Effettivamente la scelta del nome di questo ORB da parte della Sun non è stata molto felice in fatto di chiarezza ;-)

Al compilatore si deve "dare in pasto" un programma scritto appunto in IDL, che definisce solamente l'interfaccia dell'oggetto CORBA, quindi il linguaggio IDL è effettivamente un linguaggio puramente dichiarativo. Il compilatore produrrà in output dei file che descrivono questa interfaccia nel linguaggio appropriato; in questo caso si tratterà di un'interfaccia in Java, ma se si da in input lo stesso file ad un compilatore IDL per linguaggio C++ si otterrà in output un programma con una (o più) classe in C++ (probabilmente si tratterà di una classe astratta, che può essere intesa come un'interfaccia in C++).

In particolare il compilatore genererà anche i file per lo stub e per lo skeleton (come avviene nell'RMI). Poiché al compilatore viene fornita solo l'interfaccia, le classi generate dovranno essere utilizzate (tramite la derivazione o l'implementazione di un'interfaccia) per l'effettiva implementazione, che spetta sempre al programmatore sia del server che del client.

Vediamo alcuni ulteriori dettagli ovviamente con un esempio

Un esempio

Vogliamo realizzare un oggetto CORBA che restituisca la data e l'ora. Tale oggetto DateTime avrà quindi due metodi: getDate() e getTime() che restituiscono il risultato sotto forma di stringa. Inoltre vogliamo anche un metodo che restituisca un long: il numero di millisecondi (dal Gennaio del 1970), getMSsecs().

Definizione dell'interfaccia

Dobbiamo quindi scrivere l'interfaccia di tale oggetto in IDL, memorizzandola nel file DateTime.idl:

module DateTimeApp
{
    interface DateTime
    {
        string getDate();
        string getTime();
        long long getMSecs() ;
    };
};

Come si può notare il linguaggio IDL non si discosta molto dal C++ e da Java. In particolare un modulo è quello che in Java è un package, cioè una sorta di namespace che include interfacce e dichiarazioni di strutture (che non vedremo in questo primo esempio). Sulla dichiarazione dei metodi non c'è molto da dire; ovviamente si devono utilizzare i tipi dell'IDL, che mapperanno in quelli del linguaggio di destinazione, in questo caso string corrisponde a String, e long long a long (mentre long corrisponde a int). Una tabella dettagliata si può trovare nella documentazione ufficiale del jdk 1.2.

A questo punto non rimane che lanciare il compilatore (dopo averlo scaricato dal sito, averlo scompattato, e aver fatto in modo che idltojava sia in un path visibile al sistema):

idltojava -fno-cpp DateTime.idl

Il compilatore necessita di un preprocessore (che però non viene fornito dalla Sun), e di default questo compilatore sotto Windows 95 è quello del Visual C++ (!) mentre sotto Unix è cpp (quello standard del gcc). Ovviamente è anche possibile specificare tramite variabile quale altro preprocessore utilizzare; inutile dire che questo è alquanto fastidioso, in quanto non necessariamente uno ha un compilatore C++ installato! Fortunatamente per questo semplice esempio non è necessario un preprocessore, e quindi si può dire al compilatore di non utilizzarlo (tramite appunto l'opzione -fno-cpp).

Come si è detto un modulo corrisponde ad un package ed infatti il compilatore ha creato una directory DateTimeApp con diversi file:

Il client

Scriviamo adesso il client dell'oggetto CORBA; tale client si riferirà a tale oggetto come DateTime poiché questa è l'interfaccia pubblica. Il client recupererà un riferimento all'oggetto CORBA tramite l'ORB locale; una volta che si è ottenuto tale riferimento lo si deve convertire nel tipo giusto (operazione detta narrowing, in quanto si deve far scendere l'oggetto nella gerarchia, fino al punto giusto). Si ottiene infatti, come object reference, un org.omg.CORBA.Object. Il casting deve essere effettuato esplicitamente perché il runtime di Java non può sempre sapere il tipo esatto di un oggetto CORBA. Ovviamente si utilizzeranno metodi di classi di supporto (dette appunto helper), e si è visto che il compilatore ne ha creata una apposita per tale scopo (DateTimeHelper).

Ecco il listato del Client:

/*
 * Esempio di Client CORBA
 */

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

public class DateTimeClient
{
    public static void main(String args[])
    {
        try{
            // crea ed inizializza l'ORB
            ORB orb = ORB.init(args, null);

            // si ricava il root naming context
            org.omg.CORBA.Object objRef =
                orb.resolve_initial_references("NameService");
            NamingContext ncRef = NamingContextHelper.narrow(objRef);

            // Viene risolto l'Object Reference in Naming
            System.out.println( "Recupero dell'Object Reference ..." ) ;
            NameComponent nc = new NameComponent("DateTime", "");
            NameComponent path[] = {nc};
            DateTime DateTimeRef = DateTimeHelper.narrow(ncRef.resolve(path));
            System.out.println( "... Object Reference ottenuto!\n" ) ;

            // si richiama i metodi del DateTime
            System.out.println( "Chiamata metodi dell'oggetto remoto ..." ) ;
            System.out.println("Data      : " + DateTimeRef.getDate());
            System.out.println("Ora       : " + DateTimeRef.getTime());
            System.out.println("MilliSecs : " + DateTimeRef.getMSecs());

            // si crea il frame
            DateTimeClientFrame frame = new DateTimeClientFrame(DateTimeRef) ;
            frame.pack() ;
            frame.show() ;
        } catch (Exception e) {
            e.printStackTrace(System.out);
        }
    }
}

Oltre ai file del pacchetto DateTimeApp, vengono inclusi anche altri pacchetti che servono per utilizzare CORBA. In particolare si utilizzerà il naming service, tramite il quale si recupererà un riferimento all'oggetto remoto, specificandone il nome. Per ottenere un object reference al name server si utilizza il metodo resolve_initial_references passandogli la stringa "NameService" che è definita per tutti gli ORB. Per prima cosa comunque è stato inizializzato l'ORB tramite il metodo init di classe ORB. Il narrowing (cioè il cast) è effettuato richiamando il metodo narrow della classe NamingContextHelper, fornita da CORBA. A questo punto si ha effettivamente NamingContext che si utilizzerà per ottenere un riferimento ad un oggetto DateTime; questo si effettua con la chiamata ncRef.resolve(path); a tale metodo deve essere passato un array di oggetti NameComponent che rappresenta il path di DateTime sul server (che quindi deve essere conosciuto dal client). In questo caso si tratta di un array di un solo elemento. Come si vede il riferimento viene subito sottoposto a narrowing utilizzando stavolta l'helper creato dal compilatore; a questo punto si ha effettivamente un oggetto DateTime, e si possono richiamare i metodi di tale oggetto remoto. Il riferimento viene poi passato ad un frame (DateTimeClientFrame) che presenterà un'interfaccia grafica per richiamare interattivamente i metodi dell'oggetto. Anche questa classe utilizzerà questo oggetto come se fosse un oggetto standard, ignorando completamente che si tratta di un oggetto remoto.

Il server ed il servant

Il server ha una struttura simile a quella del client; la sostanziale differenza è che, poiché gestisce oggetti DateTimeClient, deve provvedere a connettere gli oggetti servant (in questo esempio ne viene istanziato solo uno) all'ORB (utilizzando il metodo connect di classe ORB) ed a registrare tale classe (interfaccia) presso il name server (in modo che i client remoti possano ottenere un object reference specificando il nome di tale classe) utilizzando il metodo rebind di classe NamingContext.

/*
 Esempio di server di oggetti CORBA
 */

import DateTimeApp.*;

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

public class DateTimeServer {

    public static void main(String args[])
    {
        try{
            // crea ed inizializza l'ORB
            ORB orb = ORB.init(args, null);

            // crea il servant e lo registra presso l'ORB
            DateTimeServant DateTimeRef = new DateTimeServant();
            orb.connect(DateTimeRef);

            // recupera il root naming context
            org.omg.CORBA.Object objRef =
                orb.resolve_initial_references("NameService");
            NamingContext ncRef = NamingContextHelper.narrow(objRef);

            // effettua il binding dell'Object Reference in Naming
            NameComponent nc = new NameComponent("DateTime", "");
            NameComponent path[] = {nc};
            ncRef.rebind(path, DateTimeRef);

            // attende richieste dal client
            java.lang.Object sync = new java.lang.Object();
            synchronized (sync) {
                sync.wait();
            }
        } catch (Exception e) {
            e.printStackTrace(System.out);
        }
    }
}

A questo punto il server deve rimanere attivo, in attesa di richieste da parte del client, e quindi si mette in attesa di essere notificato su un oggetto qualsiasi (notifica che non arriverà mai: si tratta di un espediente per non fare terminare il server); si noti che non gestirà personalmente le richieste: queste verranno gestite dal sistema dell'ORB, tramite lo skeleton.

E' importante far notare che un object reference rimane valido finché il server è attivo; sotto questo aspetto infatti gli oggetti CORBA sono oggetti transienti.

Vediamo adesso il listato del servant:

import java.util.Date ;
import java.sql.Time ;

class DateTimeServant extends _DateTimeImplBase 
{
    public String getDate() { return (new Date()).toString(); }
    public String getTime() {
        return (new Time(new Date().getTime()).toString() ) ;
    }
    public long getMSecs() { return (new Date()).getTime() ; }
}

Come si può notare si tratta di una classe semplicissima che si limita ad implementare i metodi definiti dall'interfaccia DateTime; tale interfaccia è implementata dalla classe astratta _DateTimeImplBase creata automaticamente dal compilatore e da cui deriva il servant. E' questa classe astratta che funge da skeleton e che fa la maggior parte del lavoro!

Testiamo il tutto

A questo punto siamo pronti per testare tutto quello che abbiamo scritto finora. Ovviamente di dovranno compilare tutti i .java che sono stati creati automaticamente e manualmente.

Prima di eseguire il server si deve mandare il esecuzione il name server di Java IDL (simile all'rmiregistry), specificando la porta su cui rimanere in ascolto:

tnameserv -ORBInitialPort 9999

A schermo si dovrebbe vedere una scritta (abbastanza incomprensibile) del tipo:

Initial Naming Context:
IOR:000000000000002849444c3a6f6d672e6f72672f436f734e616d696e672f4e616d696e67436f
6e746578743a312e300000000001000000000000003400010000000000086c6f72656e7a6f000401
00000000001cafabcafe0000000235f2382e00000000000000080000000000000000
TransientNameServer: setting port for initial object references to: 9999

Su un'altra finestra, a questo punto, si può lanciare il server:

java DateTimeServer -ORBInitialPort 9999

E su un'altra ancora il client

java DateTimeClient -ORBInitialPort 9999

Ottenendo a schermo il seguente output:

Recupero dell'Object Reference ...
... Object Reference ottenuto!

Chiamata metodi dell'oggetto remoto ...
Data      : Sun Sep 06 07:25:01 GMT 1998
Ora       : 07:25:08
MilliSecs : 905066709020

Il client farà poi partire una finestra grafica dalla quale si potranno richiamare i vari metodi dell'oggetto remoto:

Conclusioni

L'esempio illustrato è molto semplice: si testa il tutto in locale, e si utilizzano solo oggetti scritti in Java; tuttavia dovrebbe anche mostrare la semplicità, dopo qualche scoglio iniziale per quel che riguarda l'architettura, dell'utilizzo di CORBA. Ovviamente si può continuare ad utilizzare l'RMI, ma in questo caso non si potranno utilizzare oggetti scritti in altri linguaggi.

Vale la pena ricordare che il tutto è stato testato col jdk 1.2. beta 3, che non è certo privo di bug; quindi si dovrebbe essere abbastanza pronti ad eventuali comportamenti anomali ;-)

Negli articoli successivi continueremo a parlare di CORBA e di Java IDL.

A presto :-)

Lorenzo Bettini

Sorgenti

idlsrc.zip

Bibliografia

[1] Olindo Pindaro, Corba & Java, Una accoppiata vincente per l'integrazione di sistemi eterogenei, MokaByte Aprile 1998
[2] Ennio Grasso, Che relazione tra Java e CORBA? , MokaByte (MokaCampus) Febbraio 1998
[3] Giovanni Puliti, Remote Method Invocation (RMI), MokaByte Febbraio 1998
[4] Fabrizio Giudici, Programmazione avanzata in RMI, MokaByte Aprile 1998
[5] Bryan Morgan, Java 1.2. extends Java's distributed object capabilities, JavaWorld Aprile 1998 http://www.javaworld.com
[6] Bryan Morgan, CORBA meets Java, JavaWorld Ottobre 1997 http://www.javaworld.com