Apprendre la mise en œuvre d'une architecture CORBA en Java

Gokan EKINCI

Dans ce tutoriel, vous allez apprendre les bases de CORBA et IDL et comment les mettre en œuvre en programmation Java. C'est une formation pratique avec des travaux en atelier.

Pour réagir au contenu de ce tutoriel, un espace de dialogue vous est proposé sur le forum.

Commentez

- Titre : Apprendre la mise en œuvre d'une architecture CORBA en Java

- Auteur : Gokan EKINCI

- Parution : 12 janvier 2015

- Mise à jour : 8 août 2016

- Public visé :

- Licence : Copyright ® 2016 Gugelhupf . Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Avant de démarrer ce tutoriel…

- II. Introduction

- III. Définitions

- IV. Informations complémentaires

- V. Qu'est-ce que l'IDL ?

- VI. Les types IDL/Java

- VII. Les modules

- VIII. Les interfaces, les prototypes de méthodes, les constantes, et l'héritage

- IX. Les structures

- X. Les values type

- XI. Les alias et les tableaux

- XII. Les énumérations

- XIII. Les exceptions

- XIV. Les opérations asynchrones

- XV. Les opérations asynchrones

- XVI. TP CORBA Chapitre 1 : Génération du fichier IDL

- XVII. TP CORBA Chapitre 2 : Schéma UML

- XVIII. TP CORBA Chapitre 3 : Le modèle POA (partie serveur seulement)

- XIX. TP CORBA Chapitre 4 : Implémentation (partie serveur)

- XIX-A. Soit l'IDL créée dans le chapitre 1 du TP

- XIX-B. Générer les fichiers nécessaires avec le programme idlj

- XIX-C. Créer la classe d'implémentation CalculationServiceImpl.java

- XIX-D. Créer une classe MainServer pour l'initialisation du serveur

- XIX-E. Démarrer le serveur : Lancer orbd (processus daemon)

- XIX-F. Démarrer le programme serveur MainServer

- XX. TP CORBA Chapitre 5 : Implémentation (partie cliente)

- XXI. TP CORBA Chapitre 6 : Bonus

- XXII. Les erreurs récurrentes

- XXIII. Pour ou contre CORBA ?

- XXIV. Remerciements

I - Avant de démarrer ce tutoriel…

- Objectif à la fin de ce tutoriel : connaître l'IDL et savoir manipuler une implémentation CORBA en Java.

- Ce qu'il faut connaître :

- le langage Java.

- Connaissances appréciées, mais pas obligatoires :

- être à l'aise avec la généricité si vous souhaitez comprendre le code de la partie « Bonus » ;

- l'architecture SOA (cf. : RMI et autres protocoles réseaux de type RPC, les web services SOAP en particulier pour le fichier WSDL) ;

- un langage comme PL/SQL, PL/pgSQL, SQL/PSM ou T-SQL pour mieux comprendre le concept des modes IN, OUT et INOUT ;

- le C++ pour la syntaxe de l'IDL.

- Outil nécessaire : un ordinateur avec le JDK de Sun/Oracle (le JDK contient une implémentation CORBA).

- Pour télécharger le JDK 8 d'Oracle, cliquez ici.

- Norme CORBA utilisée dans ce cours : CORBA 2.3.1 datant d'octobre 1999 (source).

- Pour consulter l'évolution de la norme CORBA, voir ce lien : http://www.omg.org/spec/CORBA/.

- Pour consulter l'évolution de l'implémentation CORBA dans Java voir ce lien : http://docs.oracle.com/javase/8/docs/technotes/guides/idl/corba.html

II - Introduction

CORBA, acronyme de Common Object Request Broker Architecture, est une architecture logicielle, pour le développement de composants et d'Object Request Broker ou ORB. Ces composants, qui sont assemblés afin de construire des applications complètes, peuvent être écrits dans des langages de programmation distincts, être exécutés dans des processus séparés, voire être déployés sur des machines distinctes.

Source : Wikipédia

Pour qu'il y ait une communication entre un client et un serveur CORBA, il faut que le client et le serveur possèdent chacun un ORB : une application client-serveur possède au minimum deux ORB.

Un ORB est un ensemble de fonctions (classes Java, bibliothèques C++…) qui implémentent un « bus logiciel » par lequel des objets envoient et reçoivent des requêtes et des réponses, de manière transparente et portable : il s'agit de l'activation ou de l'invocation à distance par un objet, d'une méthode d'un autre objet distribué, en pratique les objets invoqués sont souvent des services.

Source : Wikipédia

Étudions le schéma suivant :

Les étapes

1. Le client CORBA invoque une méthode distante.

2. Le Stub du client CORBA marshalise l'invocation de la méthode distante en requête CORBA.

3. La requête CORBA est envoyée à travers le réseau à partir l'ORB du client.

4. La requête CORBA est réceptionnée par l'ORB du serveur.

5. Le Skeleton du serveur CORBA démarshalise la requête CORBA.

6. Le serveur CORBA exécute le service lié à la méthode invoquée.

7. Le Skeleton du serveur CORBA marshalise la réponse CORBA.

8. La réponse CORBA est envoyée à travers le réseau à partir de l'ORB du serveur.

9. La réponse CORBA est réceptionnée par l'ORB du client.

10. Le Stub du client CORBA démarshalise la réponse CORBA.

Ce que l'on peut remarquer dans ce schéma, c'est qu'il ressemble plus ou moins à celui du protocole HTTP (avec un navigateur qui joue le rôle de client et un serveur web)… donc si on fait l'analogie entre HTTP et CORBA, un client HTTP connaît le serveur grâce à une URL, mais comment un client CORBA connaît-il le serveur CORBA dans ce cas ? La réponse est l'IOR (voir sa définition dans la partie suivante)… pour faire court, l'IOR est un objet contenant plusieurs informations permettant d'identifier un « servant » (la méthode distante). Comment obtenir cet IOR ? Il existe deux moyens standards :

1. Le serveur stocke l'IOR dans un fichier et le client trouve un moyen de récupérer ce fichier pour lire son contenu. Il est possible de générer un objet proxy à partir de l'IOR grâce à une opération appelée « narrowing » ;

2. Le serveur possède un service appelé « NameService », c'est un programme qui tourne en daemon sur le serveur, il est plus connu sous le nom de « orbd » ou « tnameserv ». En utilisant le host et port du serveur et un nom attribué au service, le client peut récupérer l'IOR et génère un objet proxy.

Dans notre cas nous utiliserons la deuxième solution, car c'est la plus propre.

III - Définitions

GIOP
(General Inter ORB Protocol)

La spécification du protocole réseau utilisé par CORBA s'appelle GIOP (General Inter ORB Protocol).

- Une implémentation de GIOP est IIOP (Internet Inter ORB Protocol). IIOP utilise la couche de transport TCP/IP.

- Une autre implémentation de GIOP est MIOP (Multicast Inter ORB Protocol).

Stub
(Souche en français)

Le Stub est une portion de code côté client qui réalise l'emballage des opérations (requêtes CORBA à envoyer au serveur) et le déballage des résultats (réponse du serveur).

Skeleton
(Squelette en français)

Le Skeleton est une portion de code côté serveur qui réalise le déballage des opérations (requêtes CORBA envoyées par le client) et l'emballage des résultats (réponse à retourner au client).

Servant

Le Servant est une instance de la classe d'implémentation du service (côté serveur). La classe d'implémentation devra hériter des classes POA.

Objet Proxy

Un objet proxy est un objet qui est le représentant du servant (côté client).

IOR
(Interoperable Object Reference)

L'IOR est un objet CORBA que s'échangent le client et le serveur afin d'identifier le « servant ». Concrètement l'IOR est une référence créée par le serveur, cela permet au client de localiser le servant à partir d'un host, port, identifiant de méthode.

Narrowing
(méthode narrow(ior) )

Le « narrowing » est une opération consistant à retourner un « Objet Proxy » à partir d'une IOR.

POA
(Portable Object Adapter)

Le POA ou « Adaptateur d'objet » en français, est l'entité côté serveur qui gère les IOR et les servants.

AOM
(Active Object Map)

L'AOM est une table qui enregistre les couples IOR/Servant. L'AOM est géré par le POA.

Bus logiciel CORBA

Le bus logiciel CORBA représente l'ensemble des composants logiciels qui permettent à un client et serveur CORBA de communiquer ensemble.

Interopérabilité

L'interopérabilité représente la possibilité pour deux composants (des ORB dans le cas de CORBA) :

- …développés avec des langages différents (C++, Java, etc.) ;

- …utilisant des implémentations différentes (MICO, TAO, omniORB, etc.) ;

- …exécutés dans des environnements différents (Windows, Linux, Mac, etc.)

=> de pouvoir travailler ensemble. En gros l'interopérabilité signifie que si un client n'utilise pas la même implémentation que le serveur, ce n'est pas grave, car les deux entités pourront communiquer sans problème.

Glossaire Oracle pour CORBA (version détaillée ici) :

- idlj : est un utilitaire inclus dans le JDK pour générer des classes Java à partir d'un IDL ;

- orbd (ou tnameserv) : est un utilitaire inclus dans le JDK, il permet d'initialiser le service de nommage appelé NameService. Il faut lancer cette commande avant de démarrer le programme du serveur.

IV - Informations complémentaires

Il existe de nombreuses implémentations de CORBA, ces implémentations sont interopérables :

- Java : ORB de Sun/Oracle inclus dans Java, JacORB ;

- C++ : TAO, omniORB, MICO, ORBit ;

- Python : omniORBpy.

Vous devez bien savoir faire les différences entre :

- Skeleton et Servant : le Skeleton est le code généré par idlj, c'est la classe xxxPOA.java (ou xxxPOATie.java si on souhaite appliquer le pattern TIE), son objectif est de démarshaliser les requêtes CORBA envoyées par le Client. Le Servant est une instance de la classe d'implémentation créée par le développeur ;

- Stub et Objet Proxy : le Stub est le code généré par idlj, c'est la classe _xxxStub.java, son objectif est de marshaliser les requêtes envoyées au Serveur. L'objet Proxy est un wrapper de l'interface IDL retourné par la méthode narrow(ior), c'est l'objet dont on va se servir côté client pour envoyer des requêtes.

V - Qu'est-ce que l'IDL ?

L'IDL (Interface Definition Language) est un langage dont la syntaxe est proche de celle du C++, le principe d'un fichier IDL est le même qu'un fichier XSD (XML Schema Definition) ou WSDL : générer du code (Java, C++, Python, etc.).

Le fichier IDL représente un contrat entre le serveur et ses clients : le serveur et ses clients doivent utiliser le même fichier de contrat pour générer leur code.

L'IDL est indépendant du langage d'implémentation utilisé côté client ou serveur. On peut avoir un client en C++ et un serveur en Java par exemple.

Quelques limitations

- La surcharge de méthode n'existe pas, il n'est donc pas possible de définir deux méthodes ayant le même nom, même si leurs signatures (nom et paramètres) sont différentes.

- Il n'existe pas de moyen de représenter les templates ou la généricité avec l'IDL.

- L'IDL n'est pas sensible à la casse : il n'est donc pas possible d'avoir un nom de paramètre avec une casse différente que son type, de même que pour les noms de module et d'interface.

Exemple d'IDL :

IDL

Extrait du code Java généré à partir de l'IDL

Code IDL

1. module fr {

2. module ekinci {

3. interface CalculationService {

4. long factorial(in long num);

5. };

6. };

7. };

Code Java

1. package fr.ekinci;

3. public interface CalculationServiceOperations {

4. int factorial(int num);

5. }

Code Java

1. package fr.ekinci;

3. public interface CalculationService

4. extends CalculationServiceOperations,

5. org.omg.CORBA.Object,

6. org.omg.CORBA.portable.IDLEntity

7. { }

VI - Les types IDL/Java

IDL

Java (mode in)

Java (mode out ou inout)

void (type de retour)

void

void

boolean

boolean

org.omg.CORBA.BooleanHolder

char

char

org.omg.CORBA.CharHolder

wchar

char

org.omg.CORBA.CharHolder

octet

byte

org.omg.CORBA.ByteHolder

short

short

org.omg.CORBA.ShortHolder

unsigned short

short

org.omg.CORBA.ShortHolder

long

int

org.omg.CORBA.IntHolder

unsigned long

int

org.omg.CORBA.IntHolder

long long

long

org.omg.CORBA.LongHolder

unsigned long long

long

org.omg.CORBA.LongHolder

float

float

org.omg.CORBA.FloatHolder

double

double

org.omg.CORBA.DoubleHolder

long double

Incompatibilité en Java

Incompatibilité en Java

string

java.lang.String

org.omg.CORBA.StringHolder

wstring

java.lang.String

org.omg.CORBA.StringHolder

Object (différent de java.lang.Object ! Sert à représenter les IOR).

org.omg.CORBA.Object

org.omg.CORBA.ObjectHolder

Exemple de prototype de méthode IDL :

Code IDL

1. void meth1();

2. long meth2(in long param1);

3. string meth3(in long param1, out octet param2, inout string param3);

Les mots-clés in, out et inout sont obligatoires pour les paramètres de méthode

- in : passage par copie de valeur.

- Au niveau serveur, on va récupérer le paramètre entré par le client et s'en servir dans l'implémentation du service. Si le serveur affecte une nouvelle valeur à ce paramètre, le client n'en sera pas au courant. C'est le mode le plus simple.

- out : passage par référence.

- Le serveur ne lit pas la valeur de ce paramètre, mais peut y affecter une nouvelle valeur. Le client pourra lire la valeur affectée par le serveur.

- inout : passage par référence.

- Le serveur peut lire la valeur de ce paramètre et peut y affecter une nouvelle valeur. Le client pourra lire la valeur affectée par le serveur.

VII - Les modules

L'équivalent du package Java est module en CORBA :

IDL

Extrait du code Java généré à partir de l'IDL

Code IDL

1. module fr {

2. module ekinci {

3. // définir une interface

4. };

5. };

Code Java

1. package fr.ekinci;

3. // interface Java implémentant `IDLEntity`

À savoir :

- l'exemple IDL ci-dessus ne fonctionnera pas avec idlj, car le programme s'attend à ce qu'on ajoute au moins une interface, struct, ou enum dans le module ;

- Javadoc de l'interface IDLEntity.

VIII - Les interfaces, les prototypes de méthodes, les constantes, et l'héritage

IDL

Extrait du code Java généré à partir de l'IDL

Code IDL

1. module fr {

2. module ekinci {

4. interface Parent{

6. // Prototypes de méthode

7. void meth1();

8. long meth2(in long param1);

9. string meth3(

10. in long param1,

11. out octet param2,

12. inout string param3

13. );

15. // Constantes

16. const long MAX = 10000;

17. const float FACTOR = (10.0 - 6.5) * 3.91;

18. };

20. // Héritage

21. interface Child : Parent{};

23. };

24. };

Code Java

1. package fr.ekinci;

3. public interface ParentOperations

4. {

5. void meth1 ();

6. int meth2 (int param1);

7. String meth3 (

8. int param1,

9. org.omg.CORBA.ByteHolder param2,

10. org.omg.CORBA.StringHolder param3

11. );

12. }

Code Java

1. package fr.ekinci;

3. public interface Parent

4. extends ParentOperations,

5. org.omg.CORBA.Object,

6. org.omg.CORBA.portable.IDLEntity

7. {

8. public static final int MAX = (int)(10000);

9. public static final float FACTOR =

10. (float)((double)((double)(10.0 - 6.5) * 3.91));

11. }

Code Java

1. public interface ChildOperations

2. extends fr.ekinci.ParentOperations

3. {  }

Code Java

1. package fr.ekinci;

3. public interface Child

4. extends ChildOperations,

5. fr.ekinci.Parent,

6. org.omg.CORBA.portable.IDLEntity

7. {  }

À savoir :

- une interface ne peut pas contenir d'attribut, seulement des prototypes de méthode et des constantes ;

- la surcharge de méthode n'existe pas, il n'est donc pas possible de définir deux méthodes ayant le même nom, même si leurs signatures (nom et paramètres) sont différentes ;

- une interface ne peut pas contenir une autre interface (cf. : « nested interface » ou « interface imbriquée »).

La commande idlj.exe -fall myfile.idl va générer douze fichiers :

- Parent.java, ParentHolder.java, ParentHelper.java, ParentOperations.java, ParentPOA.java, _ParentStub.java ;

- Child.java, ChildHolder.java, ChildHelper.java, ChildOperations.java, ChildPOA.java, _ChildStub.java.

Note : l'interface ChildOperations hérite de ParentOperations, ce sont ces interfaces qui contiennent les prototypes de méthode en Java.

IX - Les structures

Les structures et valuetypes permettent de réaliser des types complexes.

IDL

Extrait du code Java généré à partir de l'IDL

Code IDL

1. module fr {

2. module ekinci {

3. struct Classe {

4. short a;

5. long b;

6. double c;

7. string d;

8. };

9. };

10. };

Code Java

1. package fr.ekinci;

3. public final class Classe

4. implements org.omg.CORBA.portable.IDLEntity {

5. public short a = (short)0;

6. public int b = (int)0;

7. public double c = (double)0;

8. public String d = null;

10. public Classe (){ } // ctor

12. public Classe (short _a, int _b, double _c, String _d) {

13. a = _a;

14. b = _b;

15. c = _c;

16. d = _d;

17. } // ctor

19. } // class Classe

À savoir :

- une structure ne peut pas contenir de prototype de méthode, seulement des attributs ;

- une structure ne peut pas hériter d'une autre structure ;

- une structure ne peut pas contenir de constante.

X - Les values type

IDL

Extrait du code Java généré à partir de l'IDL

Code IDL

1. module fr {

2. module ekinci {

3. valuetype ParentValue {

4. public long a;

5. string getFoo(in long param1);

6. };

8. valuetype ChildValue : ParentValue {

9. private double b;

10. };

11. };

12. };

Code Java

1. package fr.ekinci;

3. public abstract class ParentValue

4. implements org.omg.CORBA.portable.StreamableValue {

5. public int a = (int)0;

7. public abstract String getFoo (int param1);

9. // Voir fichier Java pour le reste

10. }

Code Java

1. package fr.ekinci;

3. public abstract class ChildValue

4. extends fr.ekinci.ParentValue {

5. protected double b = (double)0;

7. // Voir fichier Java pour le reste

8. }

À savoir :

- les valuetype ont été introduits après interface et struct ;

- contrairement aux interfaces et structures, un valuetype peut à la fois contenir des attributs et des prototypes de méthode ;

- un valuetype peut hériter d'un autre valuetype ;

- il faut obligatoirement définir un type d'accès aux attributs IDL (private ou public , protected n'existe pas).

- Une fois le code généré, on remarque que le type d'accès private en IDL devient protected en Java ;

- il est possible de précéder le mot-clé valuetype de abstract, il faudra alors enlever les attributs ainsi que le type d'accès pour les prototypes de méthode. Un abstract valuetype d'IDL est généré en tant qu'interface Java.

La commande idlj.exe -fall myfile.idl va générer huit fichiers :

- ParentValue.java, ParentValueDefaultFactory.java, ParentValueHelper.java, ParentValueHolder.java ;

- ChildValue.java, ChildValueDefaultFactory.java, ChildValueHelper.java, ChildValueHolder.java.

XI - Les alias et les tableaux

Pour créer un attribut de type tableau dans une struct IDL :

IDL

Extrait du code Java généré à partir de l'IDL

long tab[10];

public int tab[] = null;

La taille 10 est gérée dans la classe xxxHelper Pour créer un attribut de type tableau sans une limite prédéfinie dans une struct IDL il faut utiliser le mot-clé sequence :

IDL

Extrait du code Java généré à partir de l'IDL

sequence<long> integerList;

public int integerList[] = null;

Pour utiliser un tableau ou sequence en tant que paramètre dans un prototype de méthode, il faut obligatoirement passer par un alias grâce au mot-clé typedef :

IDL

Extrait du code Java généré à partir de l'IDL

1. typedef long TabInt[10][10];

2. TabInt method(in TabInt param);

int[][] method(int[][] param);

1. typedef sequence<long> TabInt;

2. TabInt method(in TabInt param);

int[] method(int[] param);

1. typedef sequence<long> TabInt;

2. TabInt method(inout TabInt param);

int[] method(fr.ekinci.TabIntHolder param);

Il est possible de placer l'instruction typedef à l'extérieur ou à l'intérieur de l'interface contenant le prototype de méthode avec le paramètre de type tableau ou sequence, cependant cette instruction doit être déclarée avant l'utilisation de l'alias.

XII - Les énumérations

IDL

Extrait du code Java généré à partir de l'IDL

1. module fr {

2. module ekinci {

3. enum Toto {

4. UN, DEUX, TROIS

5. };

6. };

7. };

1. package fr.ekinci;

3. public class Toto implements org.omg.CORBA.portable.IDLEntity

4. {

5. private int __value;

6. private static int __size = 3;

7. private static fr.ekinci.Toto[] __array =

8. new fr.ekinci.Toto [__size];

10. public static final int _UN = 0;

11. public static final fr.ekinci.Toto UN =

12. new fr.ekinci.Toto(_UN);

13. public static final int _DEUX = 1;

14. public static final fr.ekinci.Toto DEUX =

15. new fr.ekinci.Toto(_DEUX);

16. public static final int _TROIS = 2;

17. public static final fr.ekinci.Toto TROIS =

18. new fr.ekinci.Toto(_TROIS);

20. public int value ()

21. {

22. return __value;

23. }

25. public static fr.ekinci.Toto from_int (int value)

26. {

27. if (value >= 0 && value < __size)

28. return __array[value];

29. else

30. throw new org.omg.CORBA.BAD_PARAM ();

31. }

33. protected Toto (int value)

34. {

35. __value = value;

36. __array[__value] = this;

37. }

38. } // class Toto

La commande idlj.exe -fall myfile.idl va générer trois fichiers :

- Toto.java ;

- TotoHolder.java ;

- TotoHelper.java.

XIII - Les exceptions

IDL

Extrait du code Java généré à partir de l'IDL

1. module fr {

2. module ekinci {

4. // Création de l'exception

5. exception TransactionException{};

8. // raises IDL = throws Java

9. interface DistantTransaction {

10. void prepare()

11. raises (TransactionException);

13. void commit()

14. raises(TransactionException);

16. void rollback()

17. raises(TransactionException);

18. };

20. };

21. };

1. package fr.ekinci;

3. public interface DistantTransaction

4. extends DistantTransactionOperations,

5. org.omg.CORBA.Object,

6. org.omg.CORBA.portable.IDLEntity

7. {  } // interface DistantTransaction

1. package fr.ekinci;

3. public final class TransactionException

4. extends org.omg.CORBA.UserException {

6. public TransactionException () {

7. super(TransactionExceptionHelper.id());

8. } // ctor

10. public TransactionException (String $reason) {

11. super(TransactionExceptionHelper.id() + "  " + $reason);

12. } // ctor

14. } // class TransactionException

XIV - Les opérations asynchrones

À savoir :

- une exception peut contenir des paramètres :

Code IDL

1. exception MyException{

2. string reason;

3. string sentence;

4. };

- un prototype de méthode peut indiquer qu'il est possible de lancer plusieurs exceptions :

void method() raises(MyException1, MyException2, MyException3);

- Javadoc de la classe UserException

La commande idlj -fall myfile.idl va générer neuf fichiers :

- DistantTransaction.java, DistantTransactionHolder.java, DistantTransactionHelper.java, DistantTransactionOperations.java, DistantTransactionPOA.java, _DistantTransactionStub.java ;

- TransactionException.java, TransactionExceptionHolder.java, TransactionExceptionHelper.java.

XV - Les opérations asynchrones

Les méthodes CORBA sont exécutées de manière synchrone (comme pour les méthodes RMI en Java), cela signifie que si le serveur prend deux secondes pour exécuter une méthode, le client devra attendre ces deux secondes + le temps réseau que peut prendre l'échange de requête entre le client et le serveur. Cependant il existe un mot-clé oneway permettant de rendre une méthode asynchrone : le client n'attend pas que le serveur ait terminé d'exécuter la méthode oneway et passe directement à l'instruction qui suit.

Pour mettre en place une méthode asynchrone, il faut respecter quelques contraintes :

- une méthode oneway ne peut pas avoir de valeur de retour (utiliser void) ;

- une méthode oneway ne peut qu'accepter des paramètres en mode in ;

- une méthode oneway ne peut pas lancer d'exception.

Exemple :

Code IDL

1. module fr {

2. module ekinci {

3. interface Asynchronous {

5. oneway void farewell();

7. };

8. };

9. };

Liste des fichiers générés suite à l'exécution de idlj -fall myfile.idl : _AsynchronousStub.java, Asynchronous.java, AsynchronousHelper.java, AsynchronousHolder.java, AsynchronousOperations.java, AsynchronousPOA.java.

Si vous êtes familier avec les environnements multithreads et avez connaissance des problèmes de concurrences, vous savez que les méthodes asynchrones sont à manipuler avec prudence.

XVI - TP CORBA Chapitre 1 : Génération du fichier IDL

Pour générer des classes Java à partir du fichier IDL, nous allons utiliser le programme « idlj » présent dans le JDK, celui-ci porte l'extension « .exe » sous Windows. Ce programme se trouve dans le répertoire d'installation du JDK « jdkxxx/bin/ ».

Nous allons utiliser la commande suivante : idlj -fall <Insérer nom fichier idl> exemple : idlj -fall calcul.idl. Avec le paramètre -fall on va générer plusieurs fichiers de code Java pour la partie client et serveur.
Le nombre de fichiers générés dépendra des éléments utilisés dans le fichier idl. Par exemple pour chaque énumération (enum) ajoutée dans le fichier idl, idlj va générer trois fichiers Java supplémentaires.

Autrement :

- la commande idlj calcul.idl est équivalente à idlj -fclient calcul.idl et permet de générer uniquement les fichiers liés à la partie cliente ;

- la commande idlj -fserver calcul.idl permet de générer uniquement les fichiers liés à la partie serveur ;

- la commande idlj -fclient -fserver calcul.idl est équivalente à la commande idlj -fall calcul.idl et permet de générer tous les fichiers (partie cliente et serveur).

Pour plus d'informations sur le programme idlj, lire la documentation Sun/Oracle.

Créez un fichier IDL avec le contenu suivant :

IDL

Extrait du code Java généré à partir de l'IDL

Code IDL

1. module fr {

2. module ekinci {

3. interface CalculationService {

4. long factorial(in long num);

5. };

6. };

7. };

Code Java

1. package fr.ekinci;

3. public interface CalculationService

4. extends CalculationServiceOperations,

5. org.omg.CORBA.Object,

6. org.omg.CORBA.portable.IDLEntity

7. {  } // interface CalculationService

Six fichiers seront générés si on exécute la commande suivante idlj -fall calcul.idl :

Fichier

Description

CalculationServicePOA.java

Ce fichier contient une classe abstraite représentant le skeleton du serveur.

_CalculationServiceStub.java

Ce fichier contient une classe représentant le stub du client.

CalculationService.java

Ce fichier contient notre interface Java CalculationService. L'interface CalculationService hérite de CalculationServiceOperations, org.omg.CORBA.Object et org.omg.CORBA.portable.IDLEntity.

CalculationServiceHelper.java

Ce fichier contient une classe abstraite permettant de faire du narrowing et de gérer le type org.omg.CORBA.Any.

CalculationServiceHolder.java

Ce fichier contient une classe finale permettant de gérer les paramètres de méthode en mode out ou inout.

CalculationServiceOperations.java

Cette interface contient les prototypes de méthode de CalculationService, dans notre cas, il contient juste la méthode.

XVII - TP CORBA Chapitre 2 : Schéma UML

Voici un schéma UML pour avoir une meilleure vision sur la hiérarchie des classes générées :

À savoir :

- CalculationServiceImpl a été créée à la main et n'a pas été générée avec idlj. Si on décide d'utiliser le « Inheritance Model », CalculationServiceImpl doit hériter de CalculationServicePOA; si on décide d'utiliser le « Tie Delegation Model », CalculationServiceImpl ne doit pas hériter de CalculationServicePOA, mais doit implémenter l'interface CalculationServiceOperations (voir le chapitre suivant pour comprendre ces modèles) ;

- la classe CalculationServicePOATie n'est générée que si la commande suivante est exécutée : idlj -fallTIE calcul.idl. Cette classe n'est utile que si on souhaite appliquer le « Tie Delegation Model ».

XVIII - TP CORBA Chapitre 3 : Le modèle POA (partie serveur seulement)

Il existe deux variantes d'implémentation pour le modèle POA pour la partie serveur :

- l'approche par héritage (The Inheritance Model) ;

- l'approche par délégation (The Tie Delegation Model)

La différence entre ces deux modèles ?
Si on choisit le Inheritance Model, la classe d'implémentation doit obligatoirement hériter de la classe CalculationServicePOA, tandis que si on choisit le Tie Delegation Model, la classe d'implémentation doit juste implémenter l'interface CalculationServiceOperations. Comme il n'y a pas d'héritage multiple Java, le Tie Delegation Model sert à libérer un emplacement pour que la classe d'implémentation puisse hériter d'une autre classe, cependant ce modèle possède aussi un inconvénient : une méthode supplémentaire est appelée à chaque appel de méthode distante.

You might want to use the Tie model instead of the typical Inheritance model if your implementation must inherit from some other implementation. Java allows any number of interface inheritance, but there is only one slot for class inheritance. If you use the inheritance model, that slot is used up. By using the Tie Model, that slot is freed up for your own use. The drawback is that it introduces a level of indirection: one extra method call occurs when invoking a method.

Source : Sun/Oracle

L'héritage multiple n'a jamais été une solution incontournable, il est tout à fait possible d'utiliser un design pattern de comportement avec la composition pour s'en passer

La différence entre ces deux modèles de POA au niveau de l'implémentation du serveur est relativement faible, et se résume à deux lignes de codes chacune :

The Inheritance Model

Code Java

1. org.omg.CORBA.Object ref = rootpoa.servant_to_reference(calculImpl);

3. CalculationService href = CalculationServiceHelper.narrow(ref);

The Tie Delegation Model

Code Java

1. CalculationServicePOATie tie = new CalculationServicePOATie(calculImpl, rootpoa);

2. CalculationService href = tie._this(orb);

J'utiliserai le Inheritance Model dans le prochain chapitre.

Activation implicite du servant :

- dans l'approche par héritage (Inheritance Model), c'est la méthode servant_to_reference(servant) qui active implicitement notre servant ;

- dans l'approche par délégation (Tie Delegation Model), c'est la méthode _this(orb) qui active implicitement notre servant.

Les classes utiles pour :

Le client (option -fclient)

Le serveur (option -fserver)

CalculationService, CalculationServiceOperations, CalculationServiceHelper, CalculationServiceHolder, _CalculationServiceStub

CalculationService, CalculationServiceOperations, CalculationServicePOA

Si on utilise le Inheritance Model le serveur nécessite l'utilisation de la classe CalculationServiceHelper, si on utilise le Tie Delegation Model le serveur nécessite l'utilisation de la classe CalculationServiceHelper et CalculationServicePOATie, or la commande idlj -fserver calcul.idl ne génère ni CalculationServiceHelper ni CalculationServicePOATie, il faudra au choix selon le modèle choisi :

- The Inheritance Model : côté serveur, exécuter la commande idlj -fall calcul.idl pour générer CalculationServiceHelper :

- The Tie Delegation Model : côté serveur, exécuter la commande idlj -fall calcul.idl pour générer CalculationServiceHelper puis idlj -fallTIE calcul.idl pour générer CalculationServicePOATie.

Il existe un autre modèle appelé BOA, qui est l'ancêtre du modèle POA pour implémenter le code du serveur CORBA, bien que déprécié, un tutoriel sur ce modèle appelé « ImplBase » est toujours disponible pour ceux qui utilisent Java 1.3 (lien).

XIX - TP CORBA Chapitre 4 : Implémentation (partie serveur)

XIX-A - Soit l'IDL créée dans le chapitre 1 du TP

Code IDL

1. module fr {

2. module ekinci {

3. interface CalculationService {

4. long factorial(in long n);

5. };

6. };

7. };

XIX-B - Générer les fichiers nécessaires avec le programme idlj

Pour utiliser le Inheritance Model, exécuter la commande suivante idlj -fall calcul.idl.
Pour utiliser le Tie Delegation Model, exécuter la commande suivante idlj -fall calcul.idl, puis la commande suivante idlj -fallTIE calcul.idl.

La raison pour laquelle il faut exécuter les deux commandes pour le Tie Delegation Model :

Because it is not possible to generate ties and skeletons at the same time, they must be generated separately. Source : Sun/Oracle

Si vous ne souhaitez pas utiliser le Tie Delegation Model, la seconde commande n'est bien évidemment pas nécessaire.

XIX-C - Créer la classe d'implémentation CalculationServiceImpl.java

Le fichier xxxPOA.java (ou xxxPOATie.java pour une implémentation selon le pattern Tie) représente le skeleton du serveur.

Nous allons créer la classe d'implémentation CalculationServiceImpl qui hérite de CalculationServicePOA et implémente toutes les méthodes définies dans l'interface CalculationServiceOperations :

1. import fr.ekinci.CalculationServicePOA;

3. public class CalculationServiceImpl extends CalculationServicePOA {

5. @Override

6. public int factorial(int n){

7. if (n < 2) {

8. return 1;

9. }

11. int c = n;

12. while(c != 1){

13. n *= --c;

14. }

16. return n;

17. }

18. }

Vous remarquerez que pour faire simple notre implémentation de factorielle ne gère pas les cas où n est inférieur à zéro et retourne 1.

XIX-D - Créer une classe MainServer pour l'initialisation du serveur

1. import org.omg.CORBA.*;

2. import org.omg.PortableServer.*;

3. import org.omg.CosNaming.*;

4. import fr.ekinci.*;

7. /**

8. * Classe pour lancer le programme du serveur

9. * @author Gokan EKINCI

10. */

11. public class MainServer {

12. public static void main(String args[]){

13. try {

14. // Initialisation de l'ORB

15. ORB orb = ORB.init(args, null);

17. // Récupérer la référence du RootPOA et activer le POAManager

18. POA rootpoa = POAHelper.narrow(orb.resolve_initial_references("RootPOA"));

19. rootpoa.the_POAManager().activate();

21. // Créer un servant (instance de classe d'implémentation) et l'enregistrer avec l'ORB

22. CalculationServiceImpl calculImpl = new CalculationServiceImpl();

25. /* *** DEBUT INHERITANCE MODEL (vous pouvez vous référer aux chapitres précédents pour utiliser le modèle Tie Delegation Model) *** */

27. // Récupérer une référence du servant

28. org.omg.CORBA.Object servantRef = rootpoa.servant_to_reference(calculImpl);

30. CalculationService service = CalculationServiceHelper.narrow(servantRef);

32. /* *** FIN INHERITANCE MODEL *** */

34. // Récupérer la référence du service de nommage

35. org.omg.CORBA.Object nsRef = orb.resolve_initial_references("NameService");

36. NamingContextExt nce = NamingContextExtHelper.narrow(nsRef);

38. // Créer un nom pour le service et ajouter le service

39. String serviceName = "MathServices";

40. NameComponent nc[] = nce.to_name(serviceName);

41. nce.rebind(nc, service);

43. // Démarrer le service et attendre les requêtes des clients

44. System.out.println("On traite les requêtes des clients ...");

45. orb.run(); // En attente de nouveaux clients CORBA

47. } catch (Exception e){

48. System.err.println(e);

49. }

50. }

51. }

XIX-E - Démarrer le serveur : Lancer orbd (processus daemon)

Ce programme se trouve dans le même répertoire que le programme idlj, soit « /jdkxxx/bin ».

Commande pour exécuter ordb :

OS

Ligne de commande

Linux

orbd -ORBInitialPort 1050 -ORBInitialHost localhost&

Windows

start orbd -ORBInitialPort 1050 -ORBInitialHost localhost

À savoir :

- en CORBA, le numéro de port serveur par défaut est 1050 (UDP & TCP | Corba Management Agent source).
Dans notre exemple, le service de nommage va utiliser le port 1050 et fonctionner sous notre machine (cf.: localhost). Dans un environnement de production, il est déconseillé d'utiliser un port par défaut, il est conseillé d'utiliser un numéro de port non réservé (donc supérieur à 1024). En effet, si un pirate souhaite exploiter une faille connue d'une technologie, la première chose qu'il fera sera de s'attaquer à son port par défaut (ex. : 1099 pour RMI, 3306 pour MySQL, 5432 pour PostgreSQL, 1433 pour SQL Server, etc.) ;

- pour les utilisateurs Windows : le port peut être utilisé par une autre instance d'orbd, utilisez la commande netstat -na pour en savoir plus. Pour arrêter orbd, il suffira de quitter la console avec le raccourci Ctrl+C ;

- pour les utilisateurs Linux : le port peut être déjà utilisé par une autre instance d'orbd, faites un ps aux pour le voir, puis récupérer son PID pour terminer le processus avec un kill.

Aperçu sous Windows :

Un message d'avertissement Windows peut alors se lancer, autoriser l'accès :

Une nouvelle console se lance automatiquement :

Pour quitter ordb faire Ctrl+C (si vous quittez les clients ne pourront plus accéder au service).

XIX-F - Démarrer le programme serveur MainServer

Démarrer le programme du serveur avec la commande suivante s'il s'agit d'un Runnable JAR : start java -jar MainServer.jar -ORBInitialPort 1050 -ORBInitialHost localhost.

Si vous lancez le programme MainServer à partir de l'IDE Eclipse, allez dans Run Configurations… > Onglet Arguments > Program Arguments et copier/coller ceci : -ORBInitialPort 1050 -ORBInitialHost localhost.

XX - TP CORBA Chapitre 5 : Implémentation (partie cliente)

Créer une classe MainClient pour l'initialisation du client :

1. import org.omg.CORBA.*;

2. import org.omg.CosNaming.*;

3. import fr.ekinci.*;

5. /**

6. * Classe pour lancer le programme du client

7. * @author Gokan EKINCI

8. */

9. public class MainClient {

10. public static void main(String args[]){

11. try {

12. // Initialisation de l'ORB

13. ORB orb = ORB.init(args, null);

15. // Récupérer la référence du service de nommage

16. org.omg.CORBA.Object nsRef = orb.resolve_initial_references("NameService");

17. NamingContextExt nce = NamingContextExtHelper.narrow(nsRef);

19. // Générer un objet proxy

20. String serviceName = "MathServices";

21. CalculationService service = CalculationServiceHelper.narrow(nce.resolve_str(serviceName));

23. System.out.println("Réponse du serveur : " + service.factorial(5)); // 120

24. } catch (Exception e) {

25. e.printStackTrace();

26. }

27. }

28. }

Lancez le programme client à partir de votre IDE, OU BIEN compilez le programme avec javac et lancez le programme java, OU BIEN utilisez la commande suivante si vous avez transformé votre projet en JAR :
java -jar MainClient.jar -ORBInitialPort 1050 -ORBInitialHost localhost

Il est nécessaire de lancer le programme Serveur (MainServer) avant le programme Client (MainClient).

XXI - TP CORBA Chapitre 6 : Bonus

Dans les chapitres précédents, nous avons vu à quel point le code d'une implémentation CORBA peut être long à écrire… et ce procédé fastidieux sera toujours aussi répétitif. Si la mise en place du code n'était pas aussi difficile, CORBA n'aurait pas perdu sa popularité, qui sait ?

Dans cette partie nous allons réaliser le même exercice avec seulement quelques instructions.
Importer le projet corba-wrapper (lien Github) grâce à Maven (ou autres outils de gestion de dépendances) :

1. <dependency>

2. <groupId>com.github.gokan-ekinci</groupId>

3. <artifactId>corba-wrapper</artifactId>

4. <version>1.0</version>

5. </dependency>

Soit la classe CorbaServer :

Créez un projet serveur et utilisez la classe CorbaServer :

1. import fr.ekinci.corbawrapper.CorbaServer;

3. public class MainServer {

4. public static void main(String args[]) throws Exception {

5. CorbaServer server = new CorbaServer("127.0.0.1", 1050);

7. // Ajouter un service

8. server.addService("MathServices", new CalculationServiceImpl(), CalculationServiceHelper.class);

10. // Démarrer le service

11. server.run();

12. }

13. }

CalculationServiceImpl hérite de CalculationServicePOA (cf. : utilisez le Inheritance Model, PAS le Tie Delegation Model).

Soit la classe CorbaClient :

Créez un projet client et utilisez la classe CorbaClient :

1. import fr.ekinci.corbawrapper.CorbaClient;

3. public class MainClient {

4. public static void main(String args[]) throws Exception {

5. CorbaClient client = new CorbaClient("127.0.0.1", 1050);

7. // Récupérer l'objet proxy

8. CalculationService calcul = client.<CalculationService, CalculationServiceHelper>lookup("MathServices", CalculationServiceHelper.class);

10. // Invoquer des méthodes distantes

11. System.out.println("Réponse du serveur : " + calcul.factorial(5)); // 120

12. }

13. }

Lancer orbd en premier, puis le projet du serveur, puis le projet du client, le résultat obtenu sera le même que dans les chapitres précédents. Simple à mettre en place, n'est-ce pas ?

XXII - Les erreurs récurrentes

Il y a fort à parier que le programme client/serveur CORBA que nous avons mis en place fonctionne en localhost sans aucun problème, mais ce ne sera pas forcément le cas entre deux machines distinctes :

- un problème de firewall ou routeur : CORBA utilise un protocole adapté au LAN (Local Area Network). Les firewalls ou routeurs n'aiment pas forcément que deux entités qui communiquent ensemble changent dynamiquement leur numéro de port… mais c'est le comportement par défaut des protocoles utilisés par CORBA et RMI. Il existe des solutions pour fixer le numéro de port, mais nous ne les verrons pas dans ce tutoriel ;

- si vous rencontrez ce type d'exception au lancement de votre programme client sous Linux :

WARNING: "IOP00410201: (COMMFAILURE) Connection failure: socketType:
IIOPCLEAR_TEXT; hostname: 127.0.0.1;

… il suffit d'aller sur la machine du serveur et de remplacer l'adresse du localhost par l'adresse IP réelle de la machine dans le fichier /,etc./hosts, enfin redémarrez votre machine pour que les modifications soient prises en compte. Notez que ce type de problème est aussi rencontré par ceux qui font du RMI sous Linux, la solution au problème est identique.

XXIII - Pour ou contre CORBA ?

Avantages

Inconvénients

CORBA utilise un protocole binaire (GIOP/IIOP), de ce fait c'est un système plus performant (léger et rapide) que les protocoles basés sur les textes (ex. : les web services, cf. benchmark lire §3.4).

À l'heure où j'écris ce tutoriel, la dernière spécification de la norme CORBA est 3.3 (novembre 2012), mais la plupart des dernières implémentations dans les langages informatiques reposent sur la norme 2.3.1 (octobre 1999). À l'époque CORBA était considéré comme un bon substitut au DCOM de Microsoft, mais depuis d'autres technologies comme les services Web ont supplanté CORBA.

Contrairement à RMI, CORBA a été conçu pour être une solution hétérogène, il est possible d'avoir un serveur en Java et un client en C++ par exemple.

Contrairement aux services Web qui utilisent le protocole HTTP adapté au WAN (Wide Area Network), CORBA peut être bloqué par les pare-feu à cause du numéro de port qui change dynamiquement par le serveur CORBA. On limitera l'utilisation de CORBA au LAN.

La spécification 1.0 de CORBA date d'août 1991, c'est une norme mature.

La volonté de l'OMG est de rendre CORBA hétérogène, cependant le fichier IDL peut contenir des incompatibilités avec les langages d'implémentations (Java, C++, etc.). Un même IDL générant du C++ peut rencontrer des problèmes pour générer du Java par exemple (ex. : impossible de retranscrire le type long double IDL en Java).

Écrire le code d'une implémentation client/serveur CORBA est très long, très répétitif, très « old-school », mais nous avons vu qu'il suffit d'un simple wrapper comme le projet corba-wrapper pour outrepasser ces difficultés.

Aujourd'hui il existe un autre projet open source concurrent de CORBA : gRPC. Ce dernier est un protocole créé par Google, interopérable, possède une implémentation officielle pour de nombreux langages (C++, Java, Python, C#, etc.), protocole binaire (HTTP/2)… son défaut ? Qu'il ne soit pas assez connu pour le moment.
Aimeriez-vous avoir un tutoriel gRPC ? N'hésitez pas à me le dire dans les commentaires.

XXIV - Remerciements

Nous tenons à remercier Claude Leloup pour la relecture orthographique, et Malick SECK pour la mise au gabarit.