S'abonner à Techno Geek

Informatique‎ > ‎

CORBA


Informatique répartie : CORBA ( C et Java )

Par Florian GRISONI



Introduction

L'objectif de ce document est de présenter la construction d'une calculatrice en tant qu'application répartie ( mécanisme client / serveur) et cela en utilisant CORBA. Pour cela, ce document fera quelques rappels sur la construction d'applications réparties ainsi que sur CORBA. Le cœur de ce document illustre progressivement la construction de l'application.


CORBA : Common Object Request Broker Architecture

Il s'agit avant tout d'un middleware, c'est-à-dire que CORBA offre des services de haut niveau liés aux besoins de communication des applications. Le bus CORBA propose un modèle orienté objet client/serveur. La communication entre les applications ce fait donc par invocation à distance de méthodes sur les objets CORBA ( entités virtuelles gérées par le bus ).

Le serveur : c'est la partie implémentant l'objet, ici le langage C sera utilisé et cette phase a été réalisée par Nicolas GIROT.

Le client : c'est l'application utilisant l'objet, nous prendrons appui sur le langage JAVA pour réaliser les invocations à travers le bus CORBA. L'auteur de cette partie est Florian GRISONI.

Le bus CORBA permettra donc, dans notre cas, d'établir une liaison entre deux langages de programmation différent à savoir le C et le JAVA. L'invocation des méthodes se fera de manière transparente : les requêtes sembles toujours être locales. L'ORB (Object Request Broker) assure le transport des requêtes aux objets, pour cela il intègre un protocole générique GIOP et son instanciation basé sur TCP/IP : IIOP.

Afin de mettre en relation le client et le serveur de notre calculatrice, nous avons établit un contrat IDL (IDL est un langage descriptif). Le contrat est ensuite projeté, pour le client, en une interface d'invocation statique (SII) : en JAVA et en une interface de squelette static (SSI) pour le serveur : en C, nous détaillerons cette partie par la suite.

Nous allons maintenant illustrer les différentes étapes qui constituent la création d'une application répartie avec CORBA, à savoir :

  • La définition du contrat IDL.
  • La projection vers le C et le JAVA.
  • L'implémentation des interfaces IDL.
  • La création du serveur.
  • La création du client.
  • L'exécution répartie de l'application

La définition du contrat IDL

Le contrat IDL par projection permet donc une coopération entre le client et le serveur de l'application et cela en séparant l'interface de l'implémentation des objets et cela en masquant les divers problèmes liés à l'interopérabilité, l'hétérogénéité et la localisation de ceux-ci.

Voici le contrat de notre application de calculatrice sur complexe :

		
#pragma prefix "fr.insa-rouen.asi.inforep.tp3"

module Calculatrice {

exception ErreurOperation {
string raison;
} ;

struct Complexe {
float im;
float re;
} ;

interface CalculatriceComplexe {
Complexe aditioner(in Complexe g,in Complexe d);
Complexe soustraire(in Complexe g,in Complexe d);
Complexe multiplier(in Complexe g,in Complexe d);
Complexe diviser(in Complexe g,in Complexe d) raises (ErreurOperation);
} ;

} ;

Détaillons maintenant ce contrat afin de mieux comprendre comment fonctionnera notre application répartie.

#pragma permet de fixer un identifiant unique. Cet identifiant désignera notre définition au sein du référentiel des interfaces IFR. L'IFR contient une représentation des interfaces IDL accessible durant l'exécution.

Ensuite nous avons créé un module que nous avons nommé calculatrice. Un module permet de regrouper des définition de types ( constantes, type de données, interface, exception ) ou d'autres modules et permet ainsi d'éviter les confit de nom.

Au sein de notre module nous avons créé une structure (struct), groupe de champs, afin de définir un complexe. Ce complexe est donc composé de deux type de base (float) représentant ,comme peut le suggérer leur nom, les parties réel et imaginaire de celui-ci.

Afin de décrire les opérations fournies par le type d'objet CORBA CalculatriceComplexe nous avons mis en place une interface :

    • CalculatriceComplexe.

L'interface quant à elle comporte quatre opérations : additionner, soustraire, multiplier, diviser retournant un complexe. Chacune de ces opérations comporte deux attributs également de type complexe. Ces attributs sont en lecture seul : spécifié par le mot clef in.

La division par null soulève le problème de pouvoir signaler cette erreur lors de son invocation. Nous avons donc créé une exception : ErreurOperation contenant ici un champ raison qui comme son nom l'indique permet d'en préciser la raison sous la forme d'une chaine de caractères (string). Raises indique que sur l'opération diviser l'exception ErreurOperation peut être levée.

La projection vers le C et le JAVA.

Nous allons maintenant réaliser la projection de l'IDL, traduction de la spécification IDL en un langage de programmation, vers le C et le JAVA. Nous obtiendrons ainsi les souches et squellettes de l'application. Iil restera ensuite à donner un comportement aux méthodes du serveur : développement du servant.

La projection du Serveur : C

Sous linux (avec gnome) le compilateur orbit-idl est fournit et c'est pour cela que nous allons l'utiliser. De plus il est bon de noter que celui-ci est naturellement open-source mais également très rapide, utilisable depuis beaucoup de langage et qu'il possède un POA dont nous verons les détails par la suite.

La commande : orbit-idl-2 calculatrice.idl permet d'obtenir les fichiers suivants :

  • calculatrice-common.c
  • calculatrice.h
  • calculatrice-skelts.c
  • calculatrice-stubs.c

L'option –skeleton-imp permet quant à elle d'obtenir le fichier :

  • calculatrice-skelimpl.c

Regardons le fichier calculatrice.h afin de mieux voir comment a été projeté le contrat IDL.

Extrais de calculatrice.h :

typedef struct Calculatrice_ErreurOperation_type Calculatrice_ErreurOperation;
struct Calculatrice_ErreurOperation_type {
CORBA_string raison;
};

typedef struct Calculatrice_Complexe_type Calculatrice_Complexe;
struct Calculatrice_Complexe_type {
CORBA_float im;
CORBA_float re;
};

typedef CORBA_Object Calculatrice_CalculatriceComplexe;

Calculatrice_Complexe Calculatrice_CalculatriceComplexe_additioner(
Calculatrice_CalculatriceComplexe _obj,
const Calculatrice_Complexe* g,
const Calculatrice_Complexe* d,
CORBA_Environment *ev
);

Calculatrice_Complexe Calculatrice_CalculatriceComplexe_soustraire(
Calculatrice_CalculatriceComplexe _obj,
const Calculatrice_Complexe* g,
const Calculatrice_Complexe* d,
CORBA_Environment *ev
);

Calculatrice_Complexe Calculatrice_CalculatriceComplexe_multiplier(
Calculatrice_CalculatriceComplexe _obj,
const Calculatrice_Complexe* g,
const Calculatrice_Complexe* d,
CORBA_Environment *ev
);

Calculatrice_Complexe Calculatrice_CalculatriceComplexe_diviser(
Calculatrice_CalculatriceComplexe _obj,
const Calculatrice_Complexe* g,
const Calculatrice_Complexe* d,
CORBA_Environment *ev
);

Les noms IDL sont directement transcrits en noms C et sont préfixés par leur contexte IDL. Comme il n'y a pas de concept de module en C, la projection du module IDL apparait sur le nom des éléments définis au sein de ce module (en préfixant tous ces noms).

La structure Complexe est projetée en une structure :

Calculatrice_Complexe_type composée de im et re de type CORBA_ float. De plus on obtient un type Calculatrice_Complexe à l'aide d'un typedef sur struct Calculatrice_Complexe_type.
Remarque : dans la structure float est préfixé de CORBA_ puisqu'il provient du module CORBA.

Au sein du module Calculatrice nous avions défini une interface CalculatriceComplexe, celle-ci est projetée en une déclaration de type sur un CORBA_object. Les méthodes de l'interface sont elles projetées en des fonctions extern, dont le nom est précédé de Calculatrice_CalculatriceComplexe, ce qui montre leur appartenamce au module Calculatrice et à l'interface CalculatriceComplexe. Il était définit dans le contrat que ces méthodes prenaient deux arguments de type Complexe et retournaient un Complexe, dans la projection nous voyont qu'en plus des deux arguments nous avons deux autres arguments : Calculatrice_CalculatriceComplexe _obj et CORBA_Environment *ev . Ces arguments servent à l'objet et à la gestion des exceptions.

Le dernier élément de notre contrat était L'exception que nous levions en cas de division nulle. Celle-ci ce retrouve sous forme de structure Calculatrice_ErreurOperation_type qui contient la raison sous forme de CORBA_string avec une déclaration de type typedef struct Calculatrice_ErreurOperation_type Calculatrice_ErreurOperation. On obtient donc un type Calculatrice_ErreurOperation.

Maintenant que nous avons étudié de plus prés comment la projection avait été réalisée en C voyons q'en est-il en JAVA.

La projection du client : JAVA

Nous utiliserons pour la projection la distribution CORBA fournit avec le JDK, en effet, elle possède un système de nommage, elle est inclue dans la JVMet elle implémente le POA.

Le compilateur idlj ( avec l'option -fall ) fournit les fichiers suivants :

  • CalculatriceComplexe.java
  • CalculatriceComplexeHelper.java
  • CalculatriceComplexeHolder.java
  • CalculatriceComplexeOperations.java
  • CalculatriceComplexePOA.java
  • _CalculatriceComplexeStub.java
  • ComplexeHelper.java
  • ComplexeHolder.java
  • Complexe.java
  • ErreurOperationHelper.java
  • ErreurOperationHolder.java
  • ErreurOperation.java

Regardons les différents fichiers afin de mieux voir comment a été projeté le contrat IDL.

Le fichier Complexe.java :

package Calculatrice;

public final class Complexe implements org.omg.CORBA.portable.IDLEntity

{

    public float im = (float)0;

    public float re = (float)0;


    public Complexe ()

    {

    } // ctor


    public Complexe (float _im, float _re)

    {

        im = _im;

        re = _re;

     } // ctor


} // class Complexe


Le module Calculatrice a été projeté en un package Calculatrice. La structure Complexe se retrouve projetée en une classe finale du même nom. Cette classe contient deux attributs public im et re tel que dans le contrat IDL ainsi que deux constructeurs : un vide et l'autre initialisant les attributs. Mais on a également deux classes ComplexeHelper et ComplexeHolder qui servent au passage de paramétres en entrée/sortie, ainsi qu'à la gestion des types any et TypeCode de l'IDL et au transtipage via la méthode narrow contenu dans ComplexeHelper.

Le fichier CalculatriceComplexe.java de CalculatriceComplexeOperations :


package Calculatrice;


public interface CalculatriceComplexe extends CalculatriceComplexeOperations, org.omg.CORBA.Object, org.omg.CORBA.portable.IDLEntity

{

} // interface CalculatriceComplexe


package Calculatrice;


public interface CalculatriceComplexeOperations

{

    Calculatrice.Complexe additioner (Calculatrice.Complexe g, Calculatrice.Complexe d);

    Calculatrice.Complexe soustraire (Calculatrice.Complexe g, Calculatrice.Complexe d);

    Calculatrice.Complexe multiplier (Calculatrice.Complexe g, Calculatrice.Complexe d);

    Calculatrice.Complexe diviser (Calculatrice.Complexe g, Calculatrice.Complexe d) throws Calculatrice.ErreurOperation;

} // interface CalculatriceComplexeOperations


public final class ErreurOperation extends org.omg.CORBA.UserException

{

    public String raison = null;


    public ErreurOperation ()

    {

        super(ErreurOperationHelper.id());

    } // ctor


    public ErreurOperation (String _raison)

    {

        super(ErreurOperationHelper.id());

        raison = _raison;

    } // ctor



    public ErreurOperation (String $reason, String _raison)

    {

        super(ErreurOperationHelper.id() + " " + $reason);

        raison = _raison;

    } // ctor


} // class ErreurOperation


Pour l'interface CalculatriceComplexe ont obtient deux interfaces CalculatriceComplexe et CalculatriceComplexeOperations, ainsi que deux classes : CalculatriceComplexeHolder et CalculatriceComplexeHelper.

L'interface CalculatriceComplexeOperations posséde les signatures des quatres méthodes du contrat, celles-ci prennent en paramétre deux Complexe de la classe Calculatrice de même elles retournent des Complexe de même type. L'exception est projetée en une classe finale du même nom avec deux constructeurs et un attribut raison. On obtient également les classe ErreurOperationHelper et ErreurOperationHolder. Cette exception est levée par la méthodde diviser de la même manière classique via le mot clef throws, en effet il y a un héritage indirect e avec les exceptions java.

La classe _CalculatriceComplexeStub constitue la souche de notre application, c'est elle qui réalise les appelles distants.

Nous savons maintenant comment sont projetés les souches et squellettes de l'application et ceux en C comme en JAVA. La prochaine étape consiste donc à implémenter le corps du serveur et à réaliser les parties client et serveurs qui exérce la communication.

L'implémentation des interfaces IDL.

La projection du serveur étant réalisée, nous allons maintenant voir comment donner un comportement aux différentes méthodes de l'interface CalculatriceComplexe de notre contrat IDL. Cette implémentation se fait dans le fichier : calculatrice-skelimpl.c.

Les fonctions à implémenter sont :

  • impl_Calculatrice_CalculatriceComplexe_additioner
  • impl_Calculatrice_CalculatriceComplexe_soustraire
  • impl_Calculatrice_CalculatriceComplexe_multiplier
  • impl_Calculatrice_CalculatriceComplexe_diviser

Contenu du fichier Complexe.h

t

ypedef struct {

float re;

float im;

} Complexe;


Complexe complexe_creer(float re, float im);

float complexe_obtenirRe(Complexe z);

float complexe_obtenirIm(Complexe z);

void complexe_fixerRe(Complexe* z, float re);

void complexe_fixerIm(Complexe* z, float im);

Complexe complexe_additionner(Complexe z1, Complexe z2);

Complexe complexe_soustraire(Complexe z1, Complexe z2);

Complexe complexe_multiplier(Complexe z1, Complexe z2);

int complexe_diviser(Complexe z1, Complexe z2, Complexe* z);


Afin de remplir le corps de ces fonctions nous utilisons les fonctions de la librairie Complexe dont les signature sont données dans le fichier Complexe.h. Pour cela nous avons créé des fonctions qui permette le transtipage entre les Calculatrice_Complexe provenant de la projection du contrat IDL et Complexe de la librairie. On obtient les fonctions suivantes

Calculatrice_Complexe *convertirComplexe(Complexe c);

Complexe *convertirComplexeIDL(Calculatrice_Complexe c);

Le contenu de ces fonctions est précisé en annexe de ce document.

Pour les trois premiéres opérations le processus est identique :

  • On convertie les paramétres correspondant aux deux opérandes en Complexe à l'aide de la fonction convertirComplexeIDL.
  • On execute la fonction de la librairie correspondant à l'opération.
  • On convertie le résultat en Calculatrice_Complexe avec la fonction convertirComplexe
  • On retourne le résulat

Pour la fonction impl_Calculatrice_CalculatriceComplexe_diviser il faut gérer en plus l'exception que celle-ci peut levée. La fonction diviser de la librairie complexe retourne un entié : erreur, si celui-ci vaut 0 alors il y a lieu de levé une exception pour indiquer une division par NULL. Donc en cas d'erreur :

  • On effectue une allocation dynmique de l'exception (1).
  • On fixe la valeur du champ raison (2).
  • On utilise la fonction CORBA_exception_set (3) qui prend en argument la variable d'environnement que nous avons décrit précédemment, la catégorie de l'exception, le type de l'exception (0) et enfin l'exception.

Exrait de la fonction impl_Calculatrice_CalculatriceComplexe_diviser :

#define ex_erreurOperation ex_Calculatrice_ErreurOperation (0)

exception=erreurOperation__alloc(); (1)

exception.raison=CORBA_string_dup("Division par zero"); (2)

CORBA_exception_set(&ev,CORBA_USER_EXCEPTION,ex_erreurOperation,(void *)exception); (3)

Nous avons à présent étudier comment implémenter le comportement du serveur, reste à découvrir les étapes de réalisation du serveur du client à proprement parlé.

Le service de Nommage

Avant de s'attaquer directement au serveur et au client. Il est important d'étudier le Service de Nommage.

Le service Nommage (module CosNaming) définit un espace de désignation symbolique des objets. Chaque contexte maintient une liste d'associations entre des noms symboliques et des références d'objet.

Un contexte fournit notemment des opérations pour ajouter une association entre un nom et une référence (bind), rechercher la référence désignée par un chemin (resolve).

La création du serveur

Pour le serveur : nous devons écrire les programmes serveurs qui incluent l'implantation des objets et les squelettes pré-générés. Ces programmes contiennent le code pour se connecter au bus, instancier les objets racines du serveur, rendre publiques les références sur ces objets à l'aide du service Nommage et se mettre en attente de requêtes pour ces objets.

Voyons comme fonctionne notre client en C.

Les étapes sont :

  • initialisation de l'ORB (1).
  • Récupération du POA (2).
  • Activation du POAManager (2.1).
  • récupération de l'objet Service de nommage (3).
  • On enregistre le servant sous le nom Calculatrice(4).
  • On attend les requêtes (5).

PortableServer_POA rootpoa;

Calculatrice_CalculatriceComplexe corbaCalculatriceComplexeObject;

CORBA_ORB orb=NULL;

CORBA_Environment ev;

CosNaming_NamingContext nameServiceObject;

CosNaming_NameComponent namePath[1]={"Calculatrice",""};

CosNaming_Name name= {1,1,namePath,CORBA_FALSE};

CORBA_exception_init(&ev);

orb=CORBA_ORB_init(&argc,argv,"orbit-local-orb",&ev);(1)

rootpoa=(PortableServer_POA)CORBA_ORB_resolve_initial_references(orb,"RootPOA",&ev);(2)

PortableServer_POAManager_activate(PortableServer_POA__get_the_POAManager(rootpoa,&ev),&ev);(2.1)

corbaCalculatriceComplexeObject=impl_Calculatrice_CalculatriceComplexe__create(rootpoa,&ev);

nameServiceObject=CORBA_ORB_resolve_initial_references(orb,"NameService",&ev);(3)

CosNaming_NamingContext_bind(nameServiceObject,&name,corbaCalculatriceComplexeObject,&ev);(4)

CORBA_ORB_run(orb,&ev);(5)


CORBA_Object_release(corbaCalculatriceComplexeObject,&ev);

CORBA_ORB_shutdown(orb,CORBA_FALSE,&ev);

La création du client

Pour l'applications cliente: nous devons écrire un programme client qui agit sur les objets en les parcourant et en invoquant des opérations sur ceux-ci. Ce programme inclut le code des souches, le code pour l'interface Homme-Machine et le code spécifique à l'application. Le clients obtient les références des objets serveurs en consultant le service Nommage.

Voyons comme fonctionne notre client en JAVA.

Pour l'IHM nous utilisons le package fr.insarouen.asi.inforep.tpcommon. Nous créons donc une classe CalculatriceSurComplexeCorba qui constitue notre calculatrice répartie, elle implémente CalculatriceSurComplexe qui fait partie du package.

Le serveur crée donc une instance de la classe :

IHMCalculatriceCorba, qui comporte la méthode initialiserCalculatrice, laquelle instancie CalculatriceSurComplexeCorba.

Etudions le comportement de notre CalculatriceComplexe.


Les étapes sont :

  • initialisation de l'ORB (1).
  • récupération de l'objet Service de nommage (2).
  • On récupére l'objet Calculatrice auprés du service de nommage (3).
  • On applique le traitement.

ORB orb=ORB.init(args,NULL);(1)

NamingContext corbaNamingServiceObject=

NamingContextHelper.narrow(orb.resolve_initial_references("NamingService")); (2)

NameComponent calculatriceNameComponent=new NameComponent("Calculatrice","");

NameComponent calculatricePath[]={calculatriceNameComponent};

corbaCalculatriceObject=CalculatriceComplexeHelper.narrow(corbaNamingServiceObject.resolve(calculatricePath)); (3)


Pour la division on gère l'erreur comme une exception classique (voir précedemment).


La libération de la mémoire allouée par CORBA n'est pas un problème car JAVA posséde un ramasse miette.

L'exécution répartie de l'application

Le serveur :

Pour exécuter le serveur sur la machine nous devons lancer l'executable Seveur avec les options suivantes ( pour qu'il utilise les socket UNIX plutot que les fichiers ) : -ORBIIOPIPv4=1 -ORBIIOPUSock=1 -ORBInitRef NameService=IOR:Réference IOR

Avec , la référence IOR du Service de nommage donnée au lancement du service de nommage sur la machine : dans notre cas celui de la JVM : tnameserv (par défaut le port est 900, il faut donc étre en root).

Le client :

Pour éxecuter le client il faut lancer le Main dans le package tpCorba.

Comments