Compteur :
Tutoriel - Développement d'une architecture client/serveur en
Java/C++ avec CORBA

Retour Index tutoriels

 

Ce tutoriel n'est pas un cours sur CORBA. Il s'agit "simplement" de montrer comment on peut construire une architecture distribuée client/serveur basée sur CORBA.
De quoi s'agit - il ?
Le but du jeu consiste à pouvoir faire communiquer deux applications (généralement un client et un serveur) fonctionnant en local sur la même machine ou bien sur deux machines différentes connectées en réseau. Ces deux applications doivent obligatoirement être écrites dans un langage objet mais le client peut très bien être implementé en Java et le serveur en C++. C'est d'ailleurs ce que nous allons expérimenter dans ce tutoriel.
CORBA signifie Common Object Request Broker Architecture et repose sur le protocole TCP/IP. Le principe consiste à créer des objets qui seront accessibles à la fois par le client et le serveur. Par exemple le client Java pourra utiliser des méthodes d'un objet qui sont en fait implémentées dans le serveur C++.
Ce mécanisme existe déjà pour deux applications Java distantes, il s'agit de RMI (Remote Method Invocation), de même pour la plateforme .NET, la technologie s'appelle .NET Remoting. Il n'est donc pas nécessaire d'utiliser CORBA si l'on veut faire communiquer deux applications Java ou deux applications C# par exemple. En revanche, entre une application Java et une application C++, la technologie CORBA s'impose.

Afin de faire communiquer correctement nos applications à l'aide de CORBA, il est nécessaire d'installer deux ORB (Object Request Broker) : un pour la partie Java et un autre pour la partie C++. Nous avons retenu deux ORB entièrement gratuits et open source : TAO pour C++ et JacORB pour Java.

 

 

1. Installation de TAO pour la partie C++

 

Pour la partie C++, nous allons utiliser TAO (The Ace Orb), ACE signifiant Adaptive Communication Environment. TAO s'installe sur une multitude de systèmes d'exploitation, nous allons décrire l'installation sous Windows XP.
Le site officiel de TAO se trouve à l'adresse suivante :

http://www.cs.wustl.edu/~schmidt/TAO.html

TAO en version stable release 1.4 peut être téléchargé à l'adresse suivante (30 Mo environ) :

http://deuce.doc.wustl.edu/ACE-5.4+TAO-1.4.zip

  1. Enregistrer puis extraire l'archive .zip dans un répertoire au choix comme D:\Program\CORBA par exemple.

Un répertoire ACE_wrappers sera automatiquement créé sous l'arborescence D:\Program\CORBA.

  1. Créer les deux variables d'environnement suivantes avec leur valeur respective :

ACE_ROOT = D:\Program\CORBA\ACE_wrappers
TAO_ROOT = D:\Program\CORBA\ACE_wrappers\TAO

D:\Program\CORBA doit bien sûr correspondre au répertoire dans lequel l'archive .zip a été extraite.

Pour créer une variable d'environnement sous Windows XP, il faut :
- Cliquer avec le bouton droit sur Poste de travail
- Menu Propriétés
- Onglet Avancé
- Cliquer sur le bouton Variables d'environnement
- Cliquer sur le bouton Nouveau du cadre Variables système
- Entrer le nom et la valeur de la variable.

Dorénavant, dans la suite de ce tutoriel, %ACE_ROOT% et %TAO_ROOT% se réfèreront respectivement aux répertoires suivants :
D:\Program\CORBA\ACE_wrappers
D:\Program\CORBA\ACE_wrappers\TAO

  1. Modifier la variable PATH

Pour afficher la variable PATH, on procède comme suit :

- Cliquer avec le bouton droit sur Poste de travail
- Menu Propriétés
- Onglet Avancé
- Cliquer sur le bouton Variables d'environnement
- Sélectionner la variable PATH dans le cadre Variables système et appuyer sur le bouton Modifier
- A la fin du champ Valeur, rajouter (en séparant bien chaque valeur par un point virgule) :
;%ACE_ROOT%\bin;%TAO_ROOT%\orbsvcs\Naming_Service;%TAO_ROOT%\orbsvcs\CosEvent_Service

  1. Création d'un fichier config.h

Comme nous allons le voir dans l'étape 5, TAO peut être compilé sur plusieurs plateformes différentes (Windows, Linux, UNIX, etc ...). Mais auparavant, il faut créé un fichier d'entête lui indiquant sur quelle plateforme il va être compilé.

Il suffit de créer un fichier config.h dans le répertoire %ACE_ROOT%\ace dans lequel on écrit la ligne suivante :

#include "ace/config-win32.h"

  1. Compilation de TAO.

Lorsque nous avons téléchargé et décompressé TAO, il n'y a aucun binaire livré avec, seules les sources sont présentes. C'est pourquoi il est maintenant nécessaire de compiler TAO afin de générer les binaires nécessaires à son exécution.
Sous Windows, il y a plusieurs façon de compiler TAO. La plus simple consiste à utiliser Visual Studio. TAO est compatible avec les version 6 (Visual Studio 98) et 7.1 (Visual Studio .NET 2003) mais pas avec la version 7.0 (Visual Studio .NET 2002).

Remarque :

Sous Windows, j'ai essayé de compiler TAO en utilisant cygwin, mais sans succès.

Dans le cadre de ce tutoriel, nous allons utiliser la version 2003 de Visual Studio .NET pour compiler TAO.
Pour cela, il faut ouvrir le fichier %TAO_ROOT%\TAOACE.dsw et lorsqu'une boite de dialogue vous demandant de convertir et d'afficher ce projet, répondre Yes To All (Oui Pour Tous). C'est parce qu'en fait le workspace (.dsw) est au format de Visual Studio 6 et que Visual Studio .NET 2003 s'apprête à la convertir en solution (.sln) compatible avec son propre format.

En fait TAOACE.dsw est converti en TAOACE.sln qui est un fichier solution contenant pas moins de 80 projets relatifs à TAO et ACE.
Pour compiler cette solution (et par conséquent les 80 projets qu'elle contient), il suffit de faire dans Visual Studio :
Build -> Build Solution

Il faut à ce moment là prendre son mal en patience, car la compilation peut durer plus d'une heure suivant la config utilisée. J'ai toutefois remarqué un bug (sans conséquence pour la suite de ce tutoriel), c'est que 5 projets sur les 80 ne compilent pas. Pour améliorer un peu les choses, on peut refaire de nouveau Build -> Build Solution et il n'y aura plus que 2 projets sur les 80 qui ne compileront pas (rassurez vous, cette deuxième passe de compilation est beaucoup plus rapide que la première !). Ce problème est probablement dû à des problèmes d'interdépendance entre projets de la solution.

Voila, TAO est maintenant proprement installé et prêt à être utilisé, nous allons maintenant nous occuper de la partie Java.

 

2. Installation de JacORB pour la partie Java

 

Pour la partie Java, nous allons utiliser JacORB. JacORB est entièrement écrit en Java d'où sa portabilité sur tous les systèmes d'exploitation supportant Java.
Le site officiel de JacORB se trouve à l'adresse suivante :

http://www.jacorb.org

La version stable 2.2.2 de JacORB peut être téléchargée à l'adresse suivante (9 Mo) :

http://www.jacorb.org/release/2.2.2/JacORB-2_2_2-full.zip

  1. Enregistrer puis extraire l'archive .zip dans un répertoire au choix comme D:\Program\CORBA\JacORB par exemple.
  1. Installation de Ant

Avant de commencer l'installation de JacORB, il est nécessaire d'installer un outil (gratuit) nommé Ant. Ant est une sorte de Makefile pour les programmes Java mais en beaucoup plus convivial basé sur le format .xml. Ant va nous servir pendant l'installation de JacORB.
Le site officiel de Ant se trouve à l'adresse suivante :

http://ant.apache.org

La version stable 1.6.5 de Ant peut être téléchargée à l'adresse suivante (10 Mo) :

http://apache.crihan.fr/dist/ant/binaries/apache-ant-1.6.5-bin.zip

Un répertoire apache-ant-1.6.5 sera automatiquement créé sous l'arborescence D:\Program\Java.

ANT_HOME = D:\Program\Java\apache-ant-1.6.5
JAVA_HOME = D:\Program\Java\j2sdk1.4.2_06

On suppose bien sûr ici que la version du JDK de Java est la 1.4.2_06

A la fin de la valeur de la variable PATH, on rajoutera :
;%ANT_HOME%\bin
(Ne pas oublier le point-virgule qui sépare chaque valeur de la variable PATH)

  1. Installation de JacORB

JRE_HOME = D:\Program\Java\j2sdk1.4.2_06\jre
JACORB_HOME = D:\Program\CORBA\JacORB

A la fin de la valeur de la variable PATH, on rajoutera :
;%JACORB_HOME%\bin

Editer le fichier orb.properties et modifier la ligne suivante comme suit :

jacorb.config.dir=D:/Program/CORBA/JacORB

Cette ligne va signifier à JacORB d'utiliser un autre fichier de propriétés qui se trouve en fait dans %JACORB_HOME%\etc. Cet autre fichier s'appelle jacorb.properties et il peut être créé à partir du fichier jacorb_properties.template qui se trouve également dans %JACORB_HOME%\etc. Pour cela, copier le fichier jacorb_properties.template en jacorb.properties. Nous verrons ultérieurement l'utilité de ce fichier jacorb.properties.

Attention :

Normalement, lorsque on installe Java sur un PC Windows, l'installeur copie toujours java.exe et javaw.exe dans le répertoire système C:\WINDOWS\SYSTEM32. Ce n'est donc pas l'interpréteur qui se trouve ici : D:\Program\Java\j2sdk1.4.2_06\bin\java qui est utilisé. La conséquence est que le fichhier orb.properties que nous venons de copier dans %JRE_HOME%\lib ne sera pas pris en compte. Pour y remédier, il suffit dans le PATH de Windows de placer D:\Program\Java\j2sdk1.4.2_06\bin avant %SystemRoot%\system32, dans ce cas c'est bien l'interpréteur situé là où nous avons installé le JDK Java qui sera utilisé et non pas celui se situant dans le répertoire System32 de Windows.

 

Afin d'exécuter une application Java avec JacORB, nous aurons besoin d'un script nommé jaco.bat. Cependant, ce script n'existe pas par défaut à l'installation de JacORB. Nous allons le générer grâce à Ant. Pour cela, rien de plus simple : il suffit de se placer dans le répertoire %JACORB_HOME% et d'exécuter :
ant jaco

2 fichiers vont être créés : jaco et jaco.bat. Le premier correspond au script exécutable pour Linux/UNIX tandis que le second correspond au script exécutable pour Windows.

Le langage IDL (Intermediate Development Language) est, comme son nom l'indique, un langage intermédiaire permettant de décrire des objets qui vont pouvoir être implémentés en Java ou en C++ et grâce auxquels un client et un serveur vont communiquer. Un compilateur IDL permet ainsi de traduire un objet décrit en langage IDL en Java (avec le compilateur IDL de JacORB) ou en C++ (avec le compilateur IDL de TAO).
Le compilateur IDL de TAO a été généré lorsque nous avons compilé TAO. En revanche, pour JacORB nous allons le générer nous-mêmes grâce à Ant :

Il suffit de se placer dans le répertoire %JACORB_HOME% et d'exécuter :
ant idlcmd

2 fichiers vont être créés : idl et idl.bat. Le premier correspond au script exécutable pour Linux/UNIX tandis que le second correspond au script exécutable pour Windows.

 

3. Le service de nommage (ou service de noms)

 

Le service de nommage est un programme permettant d'enregistrer des objets afin qu'ils soient identifiables et utilisables par les applications clientes et serveurs.
Le programme permettant de lancer le service de nommage est :
%TAO_ROOT%\orbsvcs\Naming_Service\Naming_Service.exe
On peut lancer Naming_Service.exe de n'importe où à partir de l'invite de commande car nous avons rajouté %TAO_ROOT%\orbsvcs\Naming_Service dans le PATH.

Le problème maintenant pour les applications client et serveur consiste à localiser ce service de nommage (qui peut se trouver sur une machine distante différente de celle hébergeant le client ou le serveur par exemple), on peut le faire de deux manières, en utilisant :
- l'IOR du service de nommage
- le protocole IIOP.

IOR signifie Interoperable Object Reference, c'est un identifiant unique pour chaque objet et qui permet de les identifier sans aucune ambiguïté en fonction de leur nom, leur classe et la machine sur laquelle ils sont créés .
Le service de nommage peut lui même créer son propre identifiant IOR en le lançant de la façon suivante :

Naming_Service -o ns.ior

Le fichier ns.ior contient l'IOR du service de nommage, le client et le serveur n'auront plus qu'à lire ce fichier pour pouvoir communiquer avec ce service.
Cependant, dans le cas où les applications clients et/ou serveur se trouvent sur une machine différente de celle où a été lancé le service de nommage, il faudra instaurer doit un système de partage de fichiers, soit un serveur http afin de permettre aux applications client/serveur d'accéder à ce fameux fichier.
Pour éviter cela, nous allons utiliser la deuxième méthode : le protocole IIOP.

IIOP signifie Internet Inter-ORB Protocol. C'est une protocole au dessus de TCP/IP qui permet à plusieurs ORBs différents (comme JacORB et TAO par exemple) de communiquer entre eux.
Grâce à ce protocole, le client et le serveur vont pouvoir communiquer avec le service de nommage même s'il se trouve sur une machine différente, et ceci quelque soit l'ORB utilisé.
La ligne de commande pour utiliser le service de nommage avec IIOP est la suivante :

Naming_Service -ORBEndPoint iiop://localhost:12345

localhost désigne la machine locale (localhost désigne l'adresse IP locale : 127.0.0.1). Mais attention, dans le cas où le client et/ou le serveur se situent sur une autre machine, il faudra impérativement remplacer localhost par le nom ou l'adresse IP (dans le réseau et pas locale !!) de la machine sur laquelle est exécuté le service de nommage afin que celui-ci soit joignable par le client et/ou le serveur.
12345 représente le numéro de port de communication qui sera affecté au service de nommage.

 

4. Etude d'un premier exemple simple de client/serveur

 

Après avoir fait un peu d'administration système pour installer les couches CORBA en Java et en C++, nous allons aborder ici l'écriture d'un petit exemple simple qui va servir de fil conducteur jusqu'à la fin de ce tutoriel.
Cet exemple consiste à écrire une petite interface Java qui représente un convertisseur euro/devise. Les valeurs sont saisies dans l'interface graphique du client Java et les conversions sont effectuées dans un module serveur C++. Le serveur C++ va ensuite renvoyer les résultats à l'interface graphique du client Java qui va se charger de les afficher.
Nous allons commencer par la partie la plus dure, à savoir la partie serveur C++. Le serveur va créer un objet Euro qui va être utilisé à la fois par le serveur et le client.
Pour que cet objet soit "interprétable" à la fois par le client et le serveur, il est nécessaire de le décrire dans un premier temps à l'aide du langage IDL.

Dans le langage IDL, nous ne fournissons pas l'implémentation des objets, il s'agit juste de décrire les interfaces de ces objets tandis que l'implémentation sera effectuée dans le serveur. Le serveur va ensuite "abonner" cet objet au service de nommage et le client pourra alors l'utiliser.
Voici la description de notre objet Euro en langage IDL :

module Convertisseur
{
  interface Euro
  {
    attribute double taux;
    attribute string devise;
    double toEuro(in double devise);
    double toDevise(in double euro);
  };
};

 

module est l'équivalent de package en Java et de namespace en C++. Un module peut contenir plusieurs interfaces. Dans la partie client Java, l'interface Euro sera traduite en une interface proprement dite. Dans la partie serveur, l'interface Euro sera traduite en une classe dont les méthodes décrites dans le langage IDL seront virtuelles pures. Nous devrons obligatoirement implémenter toutes ces méthodes au sein du serveur C++.
attribute signifie que les champs taux et devise devront être accessibles en lecture et écriture. Pour cela, le compilateur IDL ca créer des accesseurs que nous implémenterons dans la partie serveur C++.
in dans les méthodes toEuro() et toDevise() signifie que les variables devise et euro devront être passées par valeur dans le sens client -> serveur.
Il existe d'autres mot-clefs comme out définissant un sens de passage d'une valeur du serveur vers le client, ce passage se faisant par adresse. Si en C++, cela ne pose aucun problème, en Java il en est tout autrement avec les types primitifs qui ne peuvent être passés que par valeur. Dans ce cas, le compilateur IDL se charge de créer des classes utilitaires en Java permettant de s'affranchir du problème.
On remarquera également les points-virgules systématiques à la fin des blocs interface et module.

Bien entendu, le langage IDL ne se limite pas à ces quelques mot-clefs et fonctionnalités. Il existe bien d'autres possibilités, nous en étudierons quelques unes tout au long de ce tutoriel.
Pour une description plus complète du langage IDL et de CORBA en général, je ne pourrais que conseiller l'excellent ouvrage (en français) :
Développer avec CORBA en Java et C++ aux éditions CampusPress.
Malheureusement, cet ouvrage n'est plus édité mais il reste disponible sous forme électronique payante.

 

 

Nous allons créer un projet Visual Studio nous permettant d'écrire, de compiler et d'exécuter le serveur C++. Ce projet va également nous permettre d'utiliser le compilateur IDL fourni avec TAO permettant de créer les modules souche ou stub (utilisés par le client) et squelette ou skeleton (utiisés par le serveur). C'est notamment le squelette qui va fournir la classe contenant les méthodes virtuelles pures correspondant à notre interface IDL que nous allons implémenter dans notre serveur. L'avantage d'utiliser Visual Studio est de pouvoir compiler l'interface IDL à la volée juste avant la compilation de nos propres classes constituant le serveur.

  1. Créer un répertoire de travail que l'on nommera ConvertisseurEuro et copier à la racine de ce répertoire un fichier
    nommé Convertisseur.idl contenant le code source IDL de notre objet Euro.
  1. Lancer Visual Studio .NET 2003
  1. Choisir File -> New -> Project puis Visual C++ Projects -> Win32 Console Project.
    Dans le champ Name, entrer Serveur comme nom de projet puis dans le champ Location, se placer dans le répertoire de travail nouvellement créé : ConvertisseurEuro.
    Appuyer sur OK puis Finish pour valider la création du projet.

Création d'un projet serveur C++ avec Visual Studio .NET 2003

  1. Dans la fenêtre Solution Explorer de Visual Studio, supprimer les fichiers automatiquement créés comme stdafx.cpp, stdafx.h et ReadMe.txt. (On n'oubliera pas de les supprimer également du disque dur). Visual Studio a également
    créé par défaut un fichier Serveur.cpp. On n'effacera pas ce fichier mais on peut en revanche effacer tout son contenu.
  1. Ajouter au projet les fichiers sources qui vont constituer notre serveur.

Notre serveur C++ est bien sûr constitué de plusieurs fichiers sources, nous allons les ajouter à notre projet même s'ils ne sont pas encore écrits.
Le fichier Serveur.cpp qui va contenir la méthode main() de notre serveur est déjà présent dans le projet, il n'y a donc rien à faire de ce côté là pour l'instant.
Notre objet Euro va être implémenté dans une classe que nous allons appeler EuroImpl et que nous allons écrire dans les fichiers EuroImpl.cpp et EuroImpl.h
Dans la fenêtre Solution Explorer de Visual Studio, cliquer avec le bouton droit dans Source Files, puis Add -> Add New Item... Choisir C++ File (.cpp) puis dans le champ Name, entrer EuroImpl.cpp.
Procéder de même avec le fichier EuroImpl.h mais dans le répertoire Header Files de la fenêtre Solution Explorer et en choisissant Header File (.h).
Enfin, rajouter les fichiers ConvertisseurS.cpp et ConvertisseurC.cpp qui correspondent respectivement au skeleton et au client qui seront automatiquement générés par le compilateur IDL de TAO.

Pour récapituler cette étape, nous pouvons dire que notre projet est constitué pour l'instant de 5 fichiers :

Serveur.cpp
EuroImpl.cpp
EuroImpl.h
ConvertisseurS.cpp
ConvertisseurC.cpp

ConvertisseurS.cpp et ConvertisseurC.cpp vont être automatiquement créés par le compilateur IDL de TAO. En revanche, nous devrons écrire nous même les 3 autres fichiers :
Serveur.cpp contient le code source de notre serveur avec, entre autres, le démarrage de l'ORB et la connexion de l'objet Euro au service de nommage.
EuroImpl.cpp et EuroImpl.h contiennent notre implémentation de l'objet Euro que nous avons décrit en langage IDL dans le fichier Convertisseur.idl.

La première étape concernant la mise en place de notre projet de serveur C++ dans Visual Studio est achevée. Dans l'étape suivante, nous allons nous consacrer à l'implémentation proprement dite de notre serveur C++, à savoir l'écriture des fichiers EuroImpl.cpp, EuroImpl.h et Serveur.cpp et enfin dans la troisième étape nous allons paramétrer les options de compilation de Visual Studio afin de compiler le serveur C++ avec notamment une compilation IDL effectuée à la volée.

 

 

Implémentation de l'objet Euro (fichiers EuroImpl.cpp et EuroImpl.h)

Tout d'abord nous allons générer un "modèle" facilitant l'écriture des prototypes des méthodes de notre objet Euro.
Nous rappelons le fichier IDL nommé Convertisseur.idl décrivant notre objet Euro

module Convertisseur
{
  interface Euro
  {
    attribute double taux;
    attribute string devise;
    double toEuro(in double devise);
    double toDevise(in double euro);
  };
};

Le but du jeu consiste ici à transformer ce fichier en un fichier C++ dans lequel nous allons implémenter notre objet Euro. C'est le compilateur IDL de TAO qui va nous permettre de réaliser cette tâche. Nous avions dit dans l'étape précédente que l'on pouvait effectuer une compilation IDL à la volée au sein de notre projet Visual Studio. Cependant, pour la toute première fois, nous allons effectuer une compilation IDL "à la main" afin de mieux comprendre les mécanisme de génération de notre objet Euro.
Lorsque nous invoquons le compilateur IDL de TAO nommé tao_idl.exe, celui ci a été compilé dynamiquement lors de la phase de compilation de TAO lui même. Il est donc impératif de lui adjoindre des librairies appartenant à notre version de Visual Studio utilisée. Pour cela nous ajouterons au PATH les 2 répertoires suivants :

D:\Program\Microsoft Visual Studio .NET 2003\Vc7\bin
D:\Program\Microsoft Visual Studio .NET 2003\Common7\IDE

Il va de soi que ces répertoires dépendent du répertoire d'installation de Visual Studio.

Ensuite, nous pouvons invoquer le compilateur IDL de TAO sur le fichier Convertisseur.idl. Ce compilateur accepte notamment l'option -GI en ligne de commande qui permet de créer le squelette de l'implémentation de notre objet Euro :

Se placer dans le répertoire Serveur que nous avons créé avec Visual Studio et exécuter la commande :

tao_idl -GI ..\Convertisseur.idl

Pas moins de ... 11 fichiers sont créés, avec notamment les fichiers ConvertisseurC.cpp et ConvertisseurS.cpp que nous avions inclus au préalable dans notre projet Visual Studio. Tous les fichiers se terminant par un C (hormis l'extension) concernent le client tandis que les fichiers se terminant par un S concernent le squelette.
L'option -GI de tao_idl a généré deux fichiers supplémentaires :
ConvertisseurI.cpp et ConvertisseurI.h qui ne sont rien moins que le début de l'implémentation de notre objet Euro.
En fait les fichiers EuroImpl.cpp et EuroImpl.h sont directement inspirés respectivement de ConvertisseurI.cpp et ConvertisseurI.h.

Voici le listing de EuroImpl.h (inspiré de ConvertisseurI.h) :

/**
* Ce fichier peut etre cree en utilisant l'option -GI de tao_idl, le fichier cree
* aura le nom : ConvertisseurI.h
*/

#ifndef _EUROIMPL_H_
#define _EUROIMPL_H_

#include <string>
#include "ConvertisseurS.h"

using namespace std;

class EuroImpl : public POA_Convertisseur::Euro
{
protected :
    string m_devise;
    double m_taux;

public :
    EuroImpl() : POA_Convertisseur::Euro() {m_devise = "Francs", m_taux = 6.55957;}
    char* devise();
    CORBA::Double taux();
    void devise(const char* _devise);
    void taux(CORBA::Double _taux);
    CORBA::Double toDevise(CORBA::Double euro);
    CORBA::Double toEuro(CORBA::Double devise);
};

#endif /* ifndef */

 

Voici le listing de EuroImpl.cpp (inspiré de ConvertisseurI.cpp mais avec l'implémentation complète de toutes les méthodes de l'objet Euro) :

/**
* Ce fichier peut etre cree en utilisant l'option -GI de tao_idl, le fichier cree
* aura le nom : ConvertisseurI.cpp
*/
#include <iostream>
#include "EuroImpl.h"

using namespace std;

char* EuroImpl::devise()
{
    /**
     * Toujours faire une copie de ce que l'on renvoie car l'ORB libere l'objet une fois
     * qu'il est renvoye (ici m_devise.c_str() qui est justement de type const char* donc
     * non modifiable).
     * Or on ne veut pas que m_devise soit modifie, d'ou la (deep) copy de l'objet renvoye.
     */
    cout << "EuroImpl::devise() - objet servant \"Euro\" -" << endl;
    return CORBA::string_dup(m_devise.c_str());
}

CORBA::Double EuroImpl::taux()
{
    cout << "EuroImpl::taux() - objet servant \"Euro\" -" << endl;
    return m_taux;
}

void EuroImpl::devise(const char* _devise)
{
    cout << "EuroImpl::devise(const char*) - objet servant \"Euro\" -" << endl;
    // Une deep copie (obligatoire) est faite grace a l'operateur '=' de la classe string
    m_devise = _devise;
}

void EuroImpl::taux(CORBA::Double _taux)
{
    cout << "EuroImpl::taux(CORBA::Double) - objet servant \"Euro\" -" << endl;
    m_taux = _taux;
}

CORBA::Double EuroImpl::toDevise(CORBA::Double euro)
{
    cout << "EuroImpl::toDevise(CORBA::Double) - objet servant \"Euro\" -" << endl;
    return euro * m_taux;
}

CORBA::Double EuroImpl::toEuro(CORBA::Double devise)
{
    cout << "EuroImpl::toEuro(CORBA::Double) - objet servant \"Euro\" -" << endl;
    return devise / m_taux;
}

Attention :

Nous avons volontairement renommé les fichiers ConvertisseurI.h et ConvertisseurI.cpp respectivement en EuroImpl.h et EuroImpl.cpp car à chaque fois que le compilateur IDL sera invoqué avec l'option -GI, il écrasera automatiquement les fichiers ConvertisseurI.h et ConvertisseurI.cpp, ce qui a pour effet immédiat d'effacer toute notre implémentation !!

 

Enfin, pour en terminer (provisoirement !) avec la partie serveur C++, nous allons nous consacrer dans la section suivante à l'écriture du serveur proprement dit.

 

Mise en place du serveur (fichier Serveur.cpp)

Dans ce paragraphe, nous allons écrire le serveur C++ proprement dit. Ce serveur va notamment instancier un objet de type EuroImpl et l'enregistrer auprès du service de nommage afin que cet objet soit accessible par le client Java.

Avant de procéder à une explication de code, je rappelerais ici quelques définitions et acronymes que nous allons utiliser :

- POA : Portable Object Adapter (ou adaptateur d'objets )
Ce composant assure la gestion des différents objets qui se situent à l'intérieur de l'application serveur.
- ORB : Object Request Broker
Framework permettant d'utiliser un architecture CORBA (TAO est un ORB C++ et JacORB est un ORB Java)
- Servant :
Objet que l'on souhaite réaliser
- Serveur :
Application qui s'exécute en proposant différents objets (les servants) aux clients
- Client :
Application qui utilise les objets distants créés par le serveur

Voici maintenant une marche à suivre (il y en a bien entendu plusieurs) pour la création du serveur, l'instanciation d'un objet EuroImpl et son enregistrement auprès du service de nommage :

  1. Initialisation de l'ORB

CORBA::ORB_var orb = CORBA::ORB_init(argc, argv, "TAO_ORB_1_4a");

 

  1. Obtention d'une référence sur l'objet RootPOA (racine de l'arborescence du POA)

CORBA::Object_var POA_object = orb->resolve_initial_references("RootPOA");
PortableServer::POA_var root_POA = PortableServer::POA::_narrow(POA_object.in());

 

  1. Définition de la stratégie qui s'applique aux différents objets gérés par notre propre POA que nous créons dans la
    foulée.

La stratégie PERSISTENT permet de garder la référence de l'objet même si le serveur est arrêté puis redémarré. C'est elle que nous allons mettre en oeuvre ici. Par défaut, c'est la stratégie TRANSIENT qui est utilisée.

CORBA::PolicyList policies;
policies.length(1);
policies[(CORBA::ULong)0] = root_POA->create_lifespan_policy(PortableServer::PERSISTENT);
PortableServer::POA_var my_POA =
    root_POA->create_POA("EuroPOA", root_POA->the_POAManager(), policies);

 

  1. Creation du servant.

Le servant est l'objet de type EuroImpl que nous souhaitons rendre disponible a d'autres applications. Nous allouons l'objet sur la pile et non sur le tas.

EuroImpl servant;

 

  1. Activation du servant auprès de notre POA (my_POA).

Il faut enregistrer et activer le servant au sein du POA que nous venons de créer car ce n'est pas un objet CORBA. C'est le POA (my_POA) qui va le publier et permettre aux applications clientes d'y accéder.

PortableServer::ObjectId_var manager_id = PortableServer::string_to_ObjectId("Euro");
my_POA->activate_object_with_id(manager_id, &servant);

 

  1. Activation du POA manager.

Le POA manager permet d'écouter les demandes des clients.

root_POA->the_POAManager()->activate();

 

  1. Enregistrement de notre objet Euro au sein du service de nommage

Une fois l'objet Euro enregistré au sein du service de nommage, les clients pourront alors s'y connecter.

  1. Obtention d'une référence au service de nommage

Lorsqu'on veut obtenir une référence au service de nommage, il faut toujours toujours utiliser l'appelation "NameService" comme dans le code ci-dessous :

CORBA::Object_var root_naming_context_obj = orb->resolve_initial_references("NameService");

  1. Obtention d'un contexte racine associé au service de nommage

CosNaming::NamingContext_var root_naming_context =
    CosNaming::NamingContext::_narrow(root_naming_context_obj);

  1. Liaison de notre propre contexte au contexte racine

CosNaming::Name euro_naming_context_name;
euro_naming_context_name.length(1);
euro_naming_context_name[0].id = CORBA::string_dup("EURO_context");
euro_naming_context_name[0].kind = CORBA::string_dup("Context");
CosNaming::NamingContext_var euro_naming_context;

try
{
    euro_naming_context = root_naming_context->bind_new_context(euro_naming_context_name);
}
catch(CosNaming::NamingContext::AlreadyBound&)
{
    // Si le contexte existe deja, cette exception sera lancee
    // Dans ce cas, il suffit de resoudre le nom de ce contexte et de recuperer le contexte
    // associe
    CORBA::Object_var obj = root_naming_context->resolve(euro_naming_context_name);
    euro_naming_context = CosNaming::NamingContext::_narrow(obj);
    if (CORBA::is_nil(euro_naming_context))
    {
        cerr << "Echec dans la restriction du contexte de nom lorsqu'il existe deja" << endl;
        return -1;
    }
}

  1. Liaison de notre objet servant (dont le nom est "Euro") à notre propre contexte

CosNaming::Name obj_name;
obj_name.length(1);
obj_name[0].id = CORBA::string_dup("Euro");
obj_name[0].kind = CORBA::string_dup("Object");

try
{
    euro_naming_context->bind(obj_name, servant._this());
}
catch(CosNaming::NamingContext::AlreadyBound&)
{
    euro_naming_context->rebind(obj_name, servant._this());
}

 

  1. Mise en attente de l'application serveur

orb->run();

La methode run() est bloquante, elle écoute les demandes des différents clients et y repond en appelant les méthodes correspondantes sur l'objet serveur.

L'implémentation de notre serveur est presque terminée. Il reste cependant un petit détail à régler.
En effet, le serveur va pouvoir être lancé dans un simple invite de commande afin de pouvoir enregistrer l'objet EuroImpl au sein du service de nommage. Cependant, si le serveur est arrêté (par un CTRL+C par exemple), l'objet EuroImpl va demeurer enregistré au sein du service de nommage, ce qui peut créer des conflits si l'on relance le serveur ultérieurement avec l'objet EuroImpl qui a été modifié entre-temps. Il faut donc veiller à désenregistrer notre objet servant une fois que l'on quitte ou interrompt le serveur :

CosNaming::Name euro_naming_context_name;
euro_naming_context_name.length(1);
euro_naming_context_name[0].id = CORBA::string_dup("EURO_context");
euro_naming_context_name[0].kind = CORBA::string_dup("Context");
root_naming_context->unbind(euro_naming_context_name);

Le fichier Serveur.cpp présente le code source complet de notre serveur.

L'écriture de notre serveur et de notre objet partagé est à présent terminée. Pour en finir définitivement avec la partie C++, il faut compiler l'ensemble grâce à Visual Studio. C'est ce que nous nous proposons de faire dans le paragraphe qui suit.

 

 

A présent, nous disposons d'une solution Visual Studio dans laquelle nous avons écrit 3 fichiers :
Serveur.cpp, EuroImpl.cpp et EuroImpl.h.
Les deux fichiers ConvertisseurS.cpp (skeleton) et ConvertisseurC.cpp (stub) sont pour l'instant vides, ils vont être automatiquement créés par le compilateur IDL que nous allons invoquer par l'intermédiaire de Visual Studio.

Voici à quoi devrait ressembler la fenêtre "Solution Explorer" de Visual Studio :

Cadre "Solution Explorer" de Visual Studio

Les étapes suivantes décrivent la marche à suivre pour mettre en oeuvre la compilation du projet :

  1. Ajout des répertoires à inclure pour la compilation

Cliquer avec le bouton droit sur le nom du projet de notre solution (ici le projet se nomme Serveur), puis sélectionner le menu contextuel Properties.
Une boite de dialogue intitulée "Serveur Property Pages" apparaît. Dans le cadre de gauche sélectionner :
Configuration Properties -> C/C++ -> General.
Dans le champ "Additional Include Directories", entrer la ligne suivante :

$(ACE_ROOT);$(TAO_ROOT);$(TAO_ROOT)\orbsvcs

 

  1. Modification des informations de debugging

Dans la boite de dialogue "Serveur Property Pages", sélectionner :
Configuration Properties -> C/C++ -> General.
Dans le champ "Debug Information Format", choisir l'option suivante :

Program Database (/Zi)

 

  1. Modification des options de génération de code

Dans la boite de dialogue "Serveur Property Pages", sélectionner :
Configuration Properties -> C/C++ -> Code Generation.
Dans la fenêtre de droite, modifier les champs comme suit :

 

  1. Modification des options de langage

Dans la boite de dialogue "Serveur Property Pages", sélectionner :
Configuration Properties -> C/C++ -> Language.
Dans le champ "Enable Run-Time Type Info", choisir : Yes (/GR)

 

  1. Réglages des "Precompiled Header"

Dans la boite de dialogue "Serveur Property Pages", sélectionner :
Configuration Properties puis C/C++ et enfin Precompiled Headers.
Dans la fenêtre de droite, modifier les champs comme suit :

 

  1. Réglages des options de l'éditeur de liens (linker)

Dans la boite de dialogue "Serveur Property Pages", sélectionner :
Configuration Properties -> Linker -> General .
Dans le champ "Output File ", entrer : $(OutDir)/Serveur.exe
Dans le champ "Enable Incremental Linking", choisir No (/INCREMENTAL:NO)
Dans le champ "Additional Library Directories", rajouter les répertoires suivants :

Liste des répertoires contenant des librairies à ajouter lors de l'édition de liens

 

Dans le champ Linker -> Input -> Additional Dependencies, rajouter les librairies suivantes :

Liste des 5 librairies à ajouter pour l'édition de liens

 

  1. Intégration du compilateur IDL à Visual Studio

Cliquer avec le bouton droit sur le nom du projet de notre solution (ici le projet se nomme Serveur), puis sélectionner le menu Add -> New Folder afin de créer un nouveau répertoire que l'on nommera IDL Files dans notre solution qui va contenir le fichier IDL Convertisseur.idl. Pour placer ce fichier dans le répertoire IDL Files, il suffit de cliquer avec le bouton droit sur ce répertoire et de choisir le menu :
Add -> Add Existing Item... et de choisir le fichier Convertisseur.idl.

Ensuite, cliquer avec le bouton droit sur le fichier Convertisseur.idl, sélectionner le menu Properties.
La boite de dialogue suivante apparaît :

Propriétés du fichier Convertisseur.idl

Dans la fenêtre de gauche, sélectionner Configuration Properties -> General, puis choisir Tool dans la fenêtre de droite. Sélectionner l'option Custom Build Tool au lieu de MIDL Tool puis cliquer sur le bouton Appliquer.
La boite de dialogue change d'aspect et les options affichées sont désormais les suivantes :

Boite de dialogue des réglages du compilateur IDL de TAO

Dans le champ "Command Line", entrer la ligne de commande servant à appeler le compilateur IDL de TAO :
$(ACE_ROOT)\bin\tao_idl ..\$(InputFileName)

Dans le champ "Description", entrer un message de description qui apparaîtra à chaque fois que le compilateur IDL sera invoqué :
Compilation de $(InputPath) avec tao_idl.exe

Dans le champ "Outputs", entrer les noms des fichiers qui vont être générés par le compilateur IDL :

$(InputName)C.i
$(InputName)C.h
$(InputName)C.cpp
$(InputName)S.i
$(InputName)S.h
$(InputName)S.cpp
$(InputName)S_T.i
$(InputName)S_T.h
$(InputName)S_T.cpp

Remarque :

$(InputFileName) est une macro prédéfinie par Visual Studio qui représente le fichier Convertisseur.idl
$(InputPath) est une macro qui représente le PATH complet du fichier Convertisseur.idl
$(InputName) est une macro qui représente le fichier idl dans extension : Convertisseur

A ce stade, le projet peut être compilé en exécutant la commande :
Build -> Build Solution
L'exécutable Serveur.exe est créé dans le répertoire Debug. Nous verrons à la fin de ce paragraphe (à la sixième étape) comment lancer l'exécution du serveur.

 

 

La mise en place d'un projet Eclipse afin de pouvoir écrire notre client est infiniment plus simple que la partie serveur avec Visual Studio (merci Java !!).

Une fois un projet (vide) que nous avons appelé Client et créé avec Eclipse, choisir le menu Project -> Properties, une boite de dialogue apparaît :

Propriétés du projet

Dans la fenêtre de gauche, sélectionner Java Build Path puis dans la fenêtre de droite cliquer sur l'onglet Libraries et sur le bouton Add External JARs...
Nous allons importer les fichiers archives nécessaires au bon fonctionnement de notre client avec l'ORB JacORB.
Il faut importer les 5 fichiers :

antlr-2.7.2.jar
avalon-framework-4.1.5.jar
concurrent-1.3.2.jar
jacorb.jar
logkit-1.2.jar

Ces fichiers se trouvent dans le sous-répertoire lib du répertoire d'installation de JacORB. Le nom de ces fichiers peut varier selon la version de JacORB utilisée. Ici, nous utilisons la version 2.2.2 de JacORB.

Nous conseillons également de supprimer les warnings liés à la sérialisation des classes ne possédant pas d'ID. Pour cela, sélectionner le menu Windows -> Preferences de Eclipse. Une boite de dialogue apparaît. Dans la fenêtre de gauche, choisir Java -> Compiler -> Errors/Warnings dans l'arborescence. Dans la fenêtre de droite, pour les erreurs/warnings de type Potential programming problems -> Serializable class without serialVersionUID, sélectionner l'option Ignore.

La première partie de la configuration d'Eclipse pour notre client est à présent achevée, nous allons maintenant nous interesser à l'écriture du client proprement dit.

 

 

  1. Compilation du fichier IDL Convertisseur.idl pour la partie Java client avec le compilateur IDL de JacORB.

Ici, nous ne sommes interessés que par l'interface client (stub), pas par l'interface serveur (skeleton). Aussi, pour générer les fichiers Java relatifs à la partie client uniquement, il suffit de se placer dans le répertoire Client créé à l'étape précédente et d'exécuter la commande :

idl.bat -noskel ..\Convertisseur.idl

Un répertoire Convertisseur est créé (il correspond au nom du module dans lequel se trouve l'interface de notre objet Euro). Dans ce répertoire les fichiers suivants sont créés :

_EuroStub.java
Euro.java
EuroHelper.java
EuroHolder.java
EuroOperations.java

Dans notre cas, nous allons exclusivement nous servir de l'interface Euro ainsi que de la classe utilitaire EuroHelper. Nous n'avons bien sûr pas besoin d'implémenter cette interface Euro puisque l'implémentation a été faite dans le serveur C++. L'interface Euro va simplement nous servir à appeler des méthodes qui ont été écrites dans la partie serveur C++.

Une fois les objets CORBA générés par le compilateur IDL de JacORB, il ne nous reste plus qu'à créer notre client (fichier Client.java). Ce client est constitué d'une interface graphique ainsi que d'une partie communication CORBA. Pour ne pas sortir du cadre de ce tutoriel, nous n'allons nous interesser qu'à la partie communication.

 

  1. Ecriture de notre client (graphique) Client.java
  1. Initialisation de l'ORB

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

  1. Obtention d'un contexte racine au service de nommage

org.omg.CORBA.Object rootNamingContextObj = orb.resolve_initial_references("NameService");
NamingContext rootNamingContext = NamingContextHelper.narrow(rootNamingContextObj);

  1. Récupération de l'objet Euro créé par le serveur à partir des contextes que nous avons également créés dans le serveur

NameComponent[] path = {
    new NameComponent(contextName, "Context"),
    new NameComponent(servantName, "Object")
};
org.omg.CORBA.Object euroObj = rootNamingContext.resolve(path);
Euro euro = EuroHelper.narrow(euroObj);

 

Nous venons ainsi de récupérer notre objet Euro qui avait été créé par le serveur C++ et nous allons (bientôt) pouvoir appeler les méthodes C++ de cet objet à partir de l'interface Euro écrite en Java.

Le code source complet du client Java se trouve dans le fichier Client.java.

Pour compiler la partie Java client avec Eclipse, il ne faut pas oublier dans la fenêtre "Package Explorer" de cliquer avec le bouton droit sur le nom du projet (Client) et de cliquer sur le menu Refresh afin d'inclure dans Eclipse les fichiers générés par le compilateur IDL.
La compilation sous Eclipse se fait automatiquement en tâche de fond à condition que l'option :
Project -> Build Automatically soit activée.

 

 

  1. Lancement du service de nommage (rappel)

On rappelle que le service de nommage se lance par la commande suivante :

Naming_Service -ORBEndPoint iiop://localhost:12345

localhost désigne la machine locale (localhost désigne l'adresse IP locale : 127.0.0.1). Mais attention, dans le cas où le client et/ou le serveur se situent sur une autre machine, il faudra impérativement remplacer localhost par le nom ou l'adresse IP (dans le réseau et pas locale !!) de la machine sur laquelle est exécuté le service de nommage afin que celui-ci soit joignable par le client et/ou le serveur.
12345 représente le numéro de port de communication qui sera affecté au service de nommage.

 

  1. Lancement du serveur C++

Le serveur C++ peut se lancer en ligne de commande de la façon suivante :

Serveur -ORBInitRef NameService=corbaloc::localhost:12345/NameService

On rappelle que l'exécutable Serveur.exe a été créé dans le répertoire Debug.

Attention :

Ici localhost représente la machine sur laquelle le service de nommage s'exécute, il faudra donc mettre l'adresse IP ou la nom de la machine adéquats si le serveur et le service de nommage s'exécutent sur des machines différentes.
De la même façon, dans le cas où le service de nommage, le serveur et le client s'exécutent sur la même machine mais que celle-ci fait partie d'un réseau local il ne faudra pas utiliser localhost mais l'adresse IP ou le nom de la machine sur le réseau.
12345 représente le numéro de port utilisé par le service de nommage.

Le serveur peut également se lancer directement à partir de Visual Studio :

 

  1. Lancement du client Java et mise en route de l'application client/serveur

Avant de lancer le client Java, il est également nécessaire de lui indiquer sur quelle machine et sur quel port se trouve le service de nommage.
Une des façons de procéder consiste à éditer le fichier de propriétés jacorb.properties qui se trouve dans le répertoire etc/ de JacORB et de rajouter la ligne :

ORBInitRef.NameService=corbaloc::localhost:12345/NameService

Les remarques à propos de localhost et du numéro de port que nous avons faites pour le serveur sont également vraies ici.

Ensuite, le client peut simplement se lancer en utilisant le fichier script jaco.bat fournit avec JacORB et qui permet de régler certaines variables d'environnement :

jaco.bat Client

Le client peut également se lancer directement à partir d'Eclipse :

Options de configuration de lancement d'une application Java sous Eclipse

Entrer Client dans les champs intitulés Name, Project et Main class. Appuyer ensuite sur le bouton Apply pour valider les entrées.

-Djava.endorsed.dirs="D:\Program\CORBA\JacORB\lib"
-Djacorb.home=D:\Program\CORBA\JacORB
-Dorg.omg.CORBA.ORBClass=org.jacorb.orb.ORB
-Dorg.omg.CORBA.ORBSingletonClass=org.jacorb.orb.ORBSingleton

En fait, dans le cadre "VM arguments", on définit des variables d'environnement spécifiques à notre client Java et c'est ce qui est fait dans le script jaco.bat.

Remarque :

C'est également à cet endroit que l'on peut rajouter l'option :
-DORBInitRef.NameService=corbaloc::localhost:12345/NameService
si l'on n'a pas modifié le fichier jacorb.properties comme indiqué plus haut.

Ainsi, une fois lancés dans l'ordre :
- le service de nommage
- le serveur
- le client
notre application distribuée devrait tourner correctement.
Si l'application ne se lance pas, cela peut venir du fait qu'il faille remplacer localhost par le nom ou l'adresse IP de la machine comme nous l'avons expliqué plus haut.

Voici l'interface graphique du client telle qu'elle devrait apparaître :

Interface graphique du client

On peut choisir la devise et sa valeur en euros et lancer des conversions de la devise vers les euros et vice-versa. Tous les calculs sont effectués dans le serveur C++ et c'est le client Java qui appelle à distance les méthodes de conversion qui sont implémentées dans le serveur

En cas de problème de connexion, JacORB nous fournit un utilitaire plutôt génial il faut dire, qui, une fois le service de nommage et le serveur lancés, permet de regarder quels sont les objets et leur contexte associés qui sont enregistrés dans le service de nommage par le serveur.
Pour cela, il suffit de taper dans l'invite de commande la ligne suivante :

nmg.bat

Une boite de dialogue Java apparaît alors, elle liste tous les objets et leur contexte associé qui sont enregistrés dans le service de nommage et auxquels le client est susceptible d'accéder :

Liste des objets et de leur contexte associé enregistrés dans le service de nommage

 

Ainsi s'achève l'écriture et la mise en oeuvre de notre premier exemple d'architecture distribuée CORBA.

L'archive premier_exemple.zip contient tous les fichiers sources de ce premier exemple.

Nous allons voir dans les paragraphes suivants que l'on peut aller beaucoup plus loin avec CORBA. Nous allons étudier quelques fonctions très utiles au sein d'exemples toujours très simples basés sur ce convertisseur euro que nous venons de mettre en place.

5. Envoi de requêtes simultanées client -> serveur (utilisation d'un pool de threads)

 

Dans cette partie, nous allons étudier un aspect très interessant concernant les requêtes multiples et le multithreading avec CORBA.
Dans un premier temps, nous allons créer un unique objet dans le serveur (comme nous l'avons fait dans notre premier exemple), mais cet objet ca être soumis à 2 requêtes simultanées provenant soit du même client (multithreadé), soit de 2 clients différents (monothreadés).
Ensuite, dans un deuxième temps, nous allons implémenter deux objets dans notre serveur. Ces deux objets seront accédés simultanément soit par un même client multithreadé, soit par 2 clients monothreadés.

 

 

Dans ce paragraphe, notre client va envoyer deux requêtes simultanées sur un objet unique créé par le serveur. Pour que deux requêtes soient simultanées, il faut que la première requête ne soit pas terminée (c'est à dire qu'elle soit encore en cours de traitement par le serveur) pendant que la deuxième est lancée par le client.

Ces deux requêtes vont s'exécuter sur un objet unique (Euro).
La première correspond à l'appel de la méthode toEuro() par le client et la deuxième par l'appel de la méthode toDevise(). Afin que toEuro() soit encore entrain d'être exécutée au moment où nous appelons toDevise(), nous allons introduire une pause de 5 secondes dans l'implémentation de la méthode toEuro() effectuée par le serveur.
Voici la méthode toEuro() modifiée dans le fichier EuroImpl.cpp :

CORBA::Double EuroImpl::toEuro(CORBA::Double devise)
{
    cout << "EuroImpl::toEuro(CORBA::Double) - objet servant \"Euro\" -" << endl;
    Sleep(5000);
    return devise / m_taux;
}

Pour que le serveur puisse accepter deux requêtes simultanées sur l'objet Euro, il va falloir créé ce que l'on appelle un pool de threads. TAO est configuré par défaut pour implémenter le modèle pool de threads. En fait, il s'agit d'une "réserve" contenant un nombre prédéfini de threads. A chaque fois qu'une requête est émise par un client, un thread est "sorti" de la réserve pour exécuter la requête puis remis en réserve aussitôt que la requête est terminée.
Nous allons remplacer l'appel orb->run() que l'on utilisait dans le fichier Serveur.cpp par un appel asynchrone à une méthode run_orb() :

ACE_Thread_Manager::instance()->spawn_n(2, (ACE_THR_FUNC)run_orb, (void *)orb,
                                        THR_DETACHED | THR_NEW_LWP);
ACE_Thread_Manager::instance()->wait();

La méthode spawn_n() avec comme premier paramètre le chiffre 2 permet de placer dans un pool de 2 threads la méthode run_orb() qui va remplacer l'appel à orb->run().
Ainsi, la méthode run_orb() pourra être appelée deux fois simultanément sans qu'il y ait blocage du serveur (il y a blocage à partir de 3 appels car notre pool de contient que deux threads au maximum).
Bien sûr, l'appel à :
ACE_Thread_Manager::instance()->wait();
permet de bloquer le thread courant du serveur pour ne pas sortir du main(), ce que faisait intrinsèquement la méthode orb->run() qu'on utilisait jusqu'à présent.
La méthode threadée run_orb() se présente comme suit :

void *run_orb(void *arg)
{
    try
    {
        CORBA::ORB_ptr orb = ACE_reinterpret_cast(CORBA::ORB_ptr, arg);
        orb->run();
    }
    catch (CORBA::Exception& ex)
    {
        ACE_ERROR((LM_ERROR, "Server::run_orb - %s\n", exception_message));
        ACE_PRINT_EXCEPTION(ex, "Exception");
        return (void *) -1;
    }

    return (void *) 0;
}

Pour lancer notre application client/serveur, il suffit de lancer le service de nommage, le serveur et le client. Ensuite, supposons que l'on veuille effectuer la conversion de 10 francs en euros, il suffit d'entrer 10 dans la zone de texte des francs puis de cliquer sur le bouton >>.
La requête est partie mais rien ne semble se passer car nous sommes en attente dans la méthode toEuro() du serveur à cause du Thread.sleep(5000) :

Nous avons lancé la conversion de 10 francs en euros mais rien de se passe

 

Si pendant l'attente, on appuie sur le bouton <<, on va convertir 0 euros (valeur par défaut dans la zone de texte des euros) en francs, c'est à dire afficher instantanément un 0 dans la zone de texte des francs en lieu et place du 10 que nous venons de rentrer manuellement :

La requête francs -> euros est en cours d'exécution alors que celle de la conversion euros -> francs est déjà terminée !

 

Ensuite, quelques secondes plus tard (5 secondes exactement après avoir appuyé sur le bouton >> pour convertir les francs en euros), nous allons voir s'afficher la valeur 1.5244 dans la zone des euros :

La conversion francs -> euros vient enfin de s'achever !

 

Nous venons bien d'exécuter deux requêtes simultanées.
Si nous n'avions pas mis en place le système de pool de threads dans le serveur, la requête consécutive à l'appui sur << (conversion euros -> francs) ne se serait exécutée qu'une fois la première conversion francs -> euros réalisée, c'est à dire une fois que la valeur 1.5244 affichée.
En sus de cela, il faut bien sûr que dans notre client Java, les appels aux méthodes toEuros() et toDevise() consécutives à l'appui sur les boutons >> et << soient asynchrones, ce que nous réalisons grâce au code suivant :

private void toDeviseCB() {
    if (COM_ACTIVE) {
        euroConv = Double.parseDouble(euroConvTField.getText());
        new Thread(new Runnable() {
            public void run() {
                deviseConv = euro.toDevise(euroConv);
                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        deviseConvTField.setText("" + deviseConv);
                    }
                });
            }
        }).start();
    }
}

private void toEuroCB() {
    if (COM_ACTIVE) {
        deviseConv = Double.parseDouble(deviseConvTField.getText());
        new Thread(new Runnable() {
            public void run() {
                euroConv = euro.toEuro(deviseConv);
                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        euroConvTField.setText("" + euroConv);
                    }
                });
            }
        }).start();
    }
}

L'archive pool_threads_1.zip contient tous les fichiers sources de cet exemple.

 

 

De façon similaire à l'exemple précédent, nous pourrions émettre nos deux requêtes simultanées à partir de deux clients distincts. Le premier client effectuerait la conversion francs -> euros en 5 secondes tandis que le second celle des euros vers les francs instantanément alors que la conversion demandée par le premier client ne serait pas encore terminée.

 

 

Dans ce paragraphe, nous allons voir un modèle un peu différent de pool de threads. Ici en fait, le serveur C++ va créer deux objets de type Euro et le client va émettre deux requêtes simultanées : une sur le premier objet et l'autre sur le second objet.
En fait, nous allons effectuer la conversion francs -> euros en émettant une requête sur l'objet Euro1 tandis que la conversion euros -> francs va d'effectuer en émettant une requête sur l'objet Euro2.

Attention :

Lorsque nous créons deux objets de type Euro, chacun possède ses propres champs indépendants. Or dans notre cas précis, le champ taux (taux de conversion devise/euros) doit être commun aux deux objets, c'est pour cela que l'on déclare le champ taux comme static dans le fichier EuroImpl.h du serveur C++.

L'archive pool_threads_2.zip contient tous les fichiers sources de cet exemple.

 

 

De façon similaire à l'exemple précédent, nous pourrions émettre nos deux requêtes simultanées à partir de deux clients distincts. Un premier client effectuerait la conversion francs -> euros en émettant une requête sur l'objet Euro1 tandis que le deuxième client effectuerait la conversion euros -> francs en émettant une requête sur l'objet Euro2.

 

6. Tests de performance : envoi et réception d'un tableau de 64 Mo de données

 

Dans cette partie, nous allons voir comment en CORBA, nous pouvons envoyer et recevoir un tableau de données. Nous allons également en profiter pour faire des mesures de performances sur l'envoi et la réception d'un bloc de données de 64 Mo.

Tout d'abord, il faut revoir le fichier Convertisseur.idl. Pour l'exemple que nous allons étudier ici, nous allons créer un nouveau fichier nommé Convertisseur3.idl dans lequel la méthode toEuro() va envoyer au serveur un tableau de flottants double précision et recevoir du serveur un autre tableau de flottants double précision :

module Convertisseur3
{
    typedef sequence<double> DoubleTab;

    interface Euro
    {
        attribute double taux;
        attribute string devise;
        DoubleTab toEuro(in DoubleTab devise);
        double toDevise(in double euro);
    };
};

Au niveau du client, pas grand chose ne change hormis le prototype de la méthode toEuro() qui doit envoyer et recevoir un tableau.
Au niveau du serveur, l'implémentation de la méthode toEuro() est bien entendu différente de celle que nous avons écrite jusqu'à présent. Pour que la conversion devise/euro continue à fonctionner entre le client et le serveur, les valeurs à convertir et à renvoyer sont stockées dans le premier élément des tableaux.
Voici l'implémentation de la méthode toEuro() dans le serveur C++ :

Convertisseur3::DoubleTab* EuroImpl::toEuro(const Convertisseur3::DoubleTab& devise)
{
    // Constructeur par copie
    Convertisseur3::DoubleTab* tableauRetour = new Convertisseur3::DoubleTab(devise);

    cout << "EuroImpl::toEuro(const Convertisseur3::DoubleTab&) - objet servant \"Euro\" -"
         << endl;
    cout << "Le tableau de dimension : " << devise.length() << " ("
         << (devise.length()*sizeof((*tableauRetour)[0])/1024/1024) << " Mo) vient d'etre
            recu par le serveur" << endl;

    // Pour le test, on change la premiere valeur de ce (gigantesque) tableau
    (*tableauRetour)[0] = (*tableauRetour)[0] / m_taux;

    cout << "Le tableau est renvoye au client par le serveur" << endl;

    // Le pointeur tableauRetour sera automatiquement libere par TAO apres l'avoir
       retourne au client
    return tableauRetour;
}

Lorsqu'on exécute ce programme (client/serveur) sur un même PC à base de Pentium M 2 Ghz, il ne faut que quelques secondes (10 au maximum) pour envoyer et recevoir ce tableau de données, ce qui prouve l'excellente tenue en terme de performances pour le couple TAO/JacORB. Il ne faut oublier que TAO ainsi que la partie serveur ont été compilées en mode debug lorsqu'on nous avons réalisé ce test.

Pour la partie Java, comme nous manipulons une quantité de données assez importante, il ne faut pas oublier d'augmenter la taille de la mémoire allouée par la JVM par la commande -Xmx512m (ici 512 Mo réservés pour la JVM).

L'archive tableaux.zip contient tous les fichiers sources de cet exemple.

 

7. Les callbacks

 

Dans tous les exemples que nous avons réalisés jusqu'à présent, la logique voulait que l'invocation des méthodes soit effectuées par un client sur un objet serveur. Les callbacks permettent d'inverser un peu cette logique et permettent à l'objet serveur d'invoquer des méthodes sur un objet client.
Nous verrons dans le paragraphe suivant que l'on peut réaliser ceci en construisant une architecture mixte client/serveur et serveur/client mais la méthode est beaucoup plus lourde à mettre en oeuvre que les callbacks.
Enfin, nous verrons un autre moyen pour un serveur d'invoquer une méthode sur un client à l'aide du service d'évènements de CORBA. Le service d'évènements permet également ce genre d'interaction entre le client et le serveur mais pour des messages beaucoup plus simples. Même si le système de callbacks peut remplacer le service d'évènements, nous recommandons d'utiliser ce service pour des messages simples. Nous étudierons le service d'évènements dans la dernière partie de ce tutoriel.

Pour faire marcher un callback, le principe consiste a créé deux nouveaux objets par rapport à l'objet Euro que nous avions créé dans le serveur.
Nous allons créer et implémenter un objet ServerObject dans le serveur et l'enregistrer dans le service de nommage. De même, nous allons créer et implémenter un objet ClientObject dans le client et l'enregistrer dans le service de nommage. Ensuite, le client invoque la méthode registerClient() de l'objet serveur ServerObject qui permet d'envoyer au serveur une référence de l'objet client ClientObject. Enfin, le serveur pourra appeler (grâce à cette référence sur cet objet ClientObject) la méthode notifyClient() de l'objet ClientObject. Nous avons bien un objet client (ClientObject) qui est rappelé par un objet serveur (ServerObject), c'est le principe du callback.

 

Voici un nouveau fichier IDL nommé Convertisseur4.idl prenant en compte les nouveaux objets nécessaires pour la mise en place du callback :

module Convertisseur4
{
    interface ClientObject
    {
        void notifyClient(in string message);
    };

    interface ServerObject
    {
        void registerClient(in ClientObject clientObj);
        oneway void callClientBack(in string message);
    };

    interface Euro
    {
        attribute double taux;
        attribute string devise;
        double toEuro(in double devise);
        double toDevise(in double euro);
    };
};

L'interface de l'objet Euro reste inchangée.
Le nouvel objet ClientObject possède une unique méthode notifyClient() qui sera appelée par le serveur.
Le nouvel objet ServerObject possède deux méthodes :
registerClient() qui permet au client d'envoyer une référence de l'objet ClientObject au serveur afin que celui-ci puisse appeler la méthode notifyClient() grâce à la référence de cette object ClientObject.
Pour les besoins de notre exemple, nous avons créé une deuxième méthode pour l'objet ServerObject : callClientBack(). Cette méthode permet justement d'invoquer notifyClient() sur l'objet ClientObject, elle sera appelée par le client sur l'objet ServerObject.
Ceci est un peu curieux (le client se rappelle lui même en fait) mais permet de simplifier un peu le code du serveur de notre exemple. Il serait bien sûr plus logique que ce soit le serveur lui même qui invoque la méthode notifyClient() plutôt que le client.
Enfin, on remarquera l'emploi du mot clé IDL oneway. Cela signifie simplement que l'appel à callClientBack() est asynchrone pour éviter les blocages.

 

L'implémentation de l'objet est assez simple et grandement facilitée par le compilateur IDL de TAO. Nous avons implémenté cet objet dans les deux fichiers : ServerObjectImpl.cpp et ServerObjectImpl.h.

 

L'enregistrement de l'objet ServerObject dans le service de nommage se fait de façon tout à fait similaire à ce que nous faisons déjà pour l'objet Euro. Le code source correpondant se trouve dans le fichier Serveur.cpp.

 

Par rapport aux clients que nous avons écris jusqu'ici dans ce tutoriel, il va falloir rajouter l'implémentation de l'objet ClientObject et son inscription dans le service de nommage.
Tout d'abord, lorsque nous appelons le compilateur IDL Java de JacORB sur le fichier Convertisseur4.idl, il faut retirer l'option -noskel que nous avions utilisée jusqu'ici pour le client Java car contrairement à ce que nous avons fait jusqu'à présent, nous allons créer un objet et l'enregistrer dans le service de nommage (le client se comporte comme un serveur d'où la nécessité des objets skeleton pour ClientObject) :

idl.bat ..\Convertisseur4.idl

L'implémentation de l'objet ClientObject lui même est des plus simples, voici le code source correspondant (fichier ClientObjectImpl.java) :

import Convertisseur4.ClientObjectPOA;

public class ClientObjectImpl extends ClientObjectPOA {

    public void notifyClient(String message) {
        System.out.println(message);
    }
}

La méthode notifyClient() se contente d'afficher un message qui sera envoyé par le serveur.

 

Cette tâche demande un peu plus de travail que les précédentes car nous n'avons jamais jusqu'à présent enregistré un objet créé par le client Java dans le service de nommage. La démarche est tout à fait similaire à ce que nous faisons dans le serveur C++ mais la différence vient de fait que nous allons utiliser JacORB et non TAO pour le faire.
Voici en détail les différentes étapes à suivre pour enregistrer l'objet ClientObject dans le service de nommage :

  1. Obtention d'une référence sur le RootPOA (racine de l'arborescence du POA) :

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

 

  1. Définition de la stratégie qui s'applique aux différents objets gérés par notre propre POA que nous créons dans la
    foulée :

Policy[] policies = {
    rootPOA.create_id_assignment_policy(IdAssignmentPolicyValue.USER_ID),
    rootPOA.create_lifespan_policy(LifespanPolicyValue.PERSISTENT)
};
POA myPOA = rootPOA.create_POA("ClientObjectPOA", rootPOA.the_POAManager(), policies);

La stratégie de type PERSISTENT permet de garder la référence de l'objet même si le serveur est arrêté puis redémarré.

 

  1. Création de notre objet ClientObject (objet servant) :

ClientObjectImpl clientObject = new ClientObjectImpl();

 

  1. Activation du servant (clientObject) au sein de notre POA (myPOA) :

clientObjectServantName = "ClientObject"
myPOA.activate_object_with_id(clientObjectServantName.getBytes(), clientObject);

 

  1. Activation du POA manager :

myPOA.the_POAManager().activate();

 

  1. Récupération du contexte "EURO_context" créé par le serveur C++ afin d'y enregistrer notre objet clientObject

NameComponent[] contextPath = {
    new NameComponent("EURO_context", "Context")
};
org.omg.CORBA.Object euroNamingContextObj = rootNamingContext.resolve(contextPath);
euroNamingContext = NamingContextHelper.narrow(euroNamingContextObj);

 

  1. Liaison de notre objet clientObject au nom "ClientObject" dans le contexte "EURO_context"

clientObjectPath = new NameComponent[] {
    new NameComponent("ClientObject", "Object")
};
try {
    euroNamingContext.bind(clientObjectPath, clientObject._this());
} catch (AlreadyBound ex) {
    euroNamingContext.rebind(clientObjectPath, clientObject._this());
}

 

Notre objet clientObject de type ClientObjectImpl est désormais enregistré dans le service de nommage sous le nom "ClientObject". Une référence de cet objet pourra être ainsi facilement envoyée par le client au serveur grâce à la méthode registerClient() de l'objet serveur serverObject :

serverObject.registerClient(clientObject._this());

Le fichier Client.java contient le code source complet du client Java.

Pour tester le bon fonctionnement de ce callback, nous invoquons la méthode :
serverObject.callClientBack() dans la méthode toEuroCB() du client graphique Java.
Cette méthode va appeler la méthode notifyClient() dans le serveur C++ et ainsi un message s'affichera dans la console Java du client à chaque fois que nous appuierons sur le bouton >> pour convertir la valeur de la devise en euros.

Comme nous l'avons remarqué plus haut, c'est le client lui même qui dit au serveur de le rappeler par la méthode notifyClient() afin d'afficher le message, ceci n'est pas très productif mais c'est un cas didactique. En fonctionnement normal, nous aurions également très bien pu appeler la méthode notifyClient() directement dans le serveur (sans passer par callClientBack()). Ceci est très utile par exemple dans le cas où le serveur vient d'effectuer une tâche et qu'il veut prévenir le client. On peut également se servir de ce genre de mécanisme lorsque le serveur effectue une tâche très longue et qu'il veut informer le client de son avancement afin de permettre à celui-ci d'afficher une barre de progression par exemple.

L'archive callbacks.zip contient tous les fichiers sources de cet exemple.

 

8. Architecture mixte : client Java / serveur C++ et client C++ / serveur Java

 

Après avoir vu le système de callbacks, nous allons étudier une autre façon d'envoyer un message dans le sens serveur C++ -> client Java.
En fait, il s'agit pour le client de se comporter en partie comme un serveur et pour le serveur de se comporter en partie comme un client. En fait, on peut dire qu'il s'agit d'une variante des callbacks étudiés dans le paragraphe précédent. Nous allons supprimer la création et l'enregistrement de l'objet ServerObject mais par souci de commodité, nous garderons le même fichier IDL Convertisseur4.idl que dans l'exemple précédent, même si l'objet ServerObject n'est pas implémenté.

En fait, le but du jeu consiste ici à créer dans le client Java un objet ClientObject et à l'enregistrer dans le service de nommage comme dans l'exemple précédent. Le serveur C++ de son côté va attendre que le client Java ait bien enregistré ClientObject auprès du service de nommage avant de le récupérer afin d'appeler la méthode notifyClient() sur cet objet. Nous envoyons ensuite un message saisi dans la console du serveur C++.
Ce système de client/serveur est finalement assez proche des callbacks que nous venons d'étudier à l'exception que c'est le serveur lui même qui rappelle le client et non pas le client qui dit au serveur de le rappeler. En outre, ce n'est pas le client qui envoie une référence de l'objet ClientObject au serveur mais ce dernier qui récupère l'objet auprès du service de nommage.

En ce qui concerne la partie client Java, rien n'a pratiquement changé par rapport à l'exemple précédent sur les callbacks hormis le fait que nous avons supprimé tout ce qui était relatif à l'objet ServerObject.
Mais comme le client Java doit également se comporter comme un serveur afin de recevoir les requêtes serveur C++ qui lui doit également se comporter comme un client, on pourrait se dire en toute logique et comme pour tous les serveurs qu'il faut démarrer l'ORB et lancer la boucle d'attente :

orb.run()

Or, le fait de lancer cette méthode run() de l'ORB dans le serveur C++ (comme nous l'avons toujours fait jusqu'à présent) suffit même si le client Java se comporte lui même comme un serveur. Il n'est donc pas nécessaire d'appeler orb.run() dans la partie Java.

Pour la partie serveur (et client) C++ en revanche, les modifications à apporter par rapport à l'exemple précédent
sont un peu plus importantes :

Il est nécessaire que le l'ORB soit lancé de façon asynchrone (non bloquante) dans le serveur C++ car une fois l'ORB lancé, le serveur doit envoyer au client un message saisi dans la console. Or il faut bien que l'on ait la main afin de saisir le message à envoyer dans la console !
Nous allons donc lancer l'ORB TAO comme nous l'avions fait dans la partie 5 consacrée aux requêtes multiples, en utilisant un pool de threads un peu particulier constitué d'un unique thread :

ACE_Thread_Manager::instance()->spawn((ACE_THR_FUNC)run_orb, (void *)orb,
                                       THR_DETACHED | THR_NEW_LWP);

La méthode run_orb() étant la méthde threadée (ou asynchrone) utilisée pour lancée l'ORB :

void *run_orb(void *arg)
{
    try
    {
        CORBA::ORB_ptr orb = ACE_reinterpret_cast(CORBA::ORB_ptr, arg);
        orb->run();
    }
    catch (CORBA::Exception& ex)
    {
        ACE_ERROR ((LM_ERROR, "Server::run_orb - %s\n", exception_message));
        ACE_PRINT_EXCEPTION(ex, "Exception");
        return (void *) -1;
    }

    return (void *) 0;
}

 

Une fois l'ORB lancé de façon non bloquante, la partie de code suivante va interroger le service de nommage dans une boucle infinie jusqu'à ce que la partie serveur du client Java enregistre l'objet ClientObject.

// Recuperation d'un pointeur sur l'objet ClientObject qui est implemente par le
// serveur Java
// Cet objet se situe dans le meme contexte (de nom "EURO_context") que celui utilise par
// le serveur C++ pour les objets Euro et ServerObject. Ce contexte est stocke dans la
// variable "euro_naming_context" du programme C++
CosNaming::Name client_obj_name;
client_obj_name.length(1);
client_obj_name[0].id = CORBA::string_dup("ClientObject");
client_obj_name[0].kind = CORBA::string_dup("Object");
CORBA::Object_var obj;
Convertisseur4::ClientObject_var client_object_servant;
bool waiting_connexion = true;

// On boucle jusqu'a ce que le serveur Java soit lance
// On detecte que le serveur Java a ete lance lorsque l'objet ClientObject aura ete
// enregistre par le serveur Java dans le service de nommage
while(waiting_connexion)
{
    try
    {
        Sleep(2000); // Attente de 2 secondes entre chaque tentative
        obj = euro_naming_context->resolve(client_obj_name);
        waiting_connexion = false;
    }
    catch (CosNaming::NamingContext::NotFound&)
    {
        // Si exception c'est que le serveur Java n'est pas encore lance
        cout << "Attente du lancement du Client/Serveur Java" << endl;
    }
}

client_object_servant = Convertisseur4::ClientObject::_narrow(obj);
cout << "Connexion au serveur Java OK" << endl;

 

Une fois l'ORB lancé et l'objet ClientObject recupéré par la partie client sur serveur C++, nous allons envoyer un message à la partie serveur du client Java :

while(true)
{
    char message[100];

    strcpy(message, "");

    // Saisie du message a envoyer au serveur Java
    cout << "Saisir le message a envoyer au serveur Java : " << endl;

    // Attention :
    // cin recupere ce qui vient de la console : les espaces sont transformes en retour
    // chariot
    cin >> message;

    // Un CTRL+C fait que message = ""
    // On met alors le thread courant en attente car un signal d'interruption (SIGINT)
    // va etre lance et il faut laisser la methode deconnexion() s'executer
    if (strcmp(message, "") == 0)
    {
        ACE_Thread_Manager::instance()->wait();
    }

    // Envoi du message au serveur Java
    exception_message = "Echec lors de l'appel de notifyClient()";
    client_object_servant->notifyClient(CORBA::string_dup(message));
} // fin while(true)

Notre architecture mixte client/serveur est désormais opérationnelle. Pour la tester, il suffit de lancer la partie C++, puis la partie Java. Une fois la connexion établie, on peut saisir le message à envoyer dans la console du C++ et après avoir validé par Entrée, ce message sera affiché dans la console du Java.

Remarque :

Un petit bug subsiste toutefois dans notre application.
En effet, supposons que la partie C++ ait envoyé un message au Java et que l'on quitte l'application Java de suite après. Cela causerait la désinscription immédiate de l'objet ClientObject du service de nommage. Ensuite, si l'on veut de nouveau envoyer un message au Java, la partie C++ ne vérifie plus que l'objet ClientObject est toujours enregistré dans le service de nommage, ce qui va causer une exception. Pour que tout soit parfait ou presque, il faudrait que le C++ vérifie en permanence (avant chaque envoi de message) que l'objet ClientObject soit toujours enregistré dans le service de nommage.

L'archive architecture_mixte.zip contient tous les fichiers sources de cet exemple.

 

9. Le service d'évènements

 

Pour terminer ce (très) long tutoriel, nous allons donner un exemple de fonctionnement d'un service CORBA appelé service d'évènement. Ce service joue un peu le rôle des callbacks que nous avons étudiés plus haut, c'est à dire qu'il permet au serveur d'envoyer des messages simples vers le client.
L'utilisation de ce service est préconisée lors de l'envoi de notifications simples au client car dans ce cas le codage demeure générique. Pour des messages plus lourds et plus complexes dans le sens serveur -> client, on privilégiera l'approche moins générique et plus spécifique des callbacks.

Le principe de fonctionnement du service d'évènements est basé sur un canal d'évènements dans lequel des fournisseurs émettent des données tandis que des consommateurs reçoivent ces données. Le serveur va jouer ici le rôle de fournisseur : il va émettre des données vers le canal d'évènements tandis que le client va jouer le rôle de consommateur qui est prévenu par le canal de la réception des données.
Ce modèle est appelé modèle push. Il existe un autre modèle nommé pull que nous n'étudierons pas ici car il est beaucoup plus gourmand en CPU que le modèle push.
Dans ce modèle pull, le serveur joue donc le rôle de fournisseur ou push supplier tandis que le client joue le rôle de consommateur ou push consumer.

Les clients et serveurs sont isolés les uns des autres par des proxies internes du service d'évènements. Le serveur ne dialogue qu'avec son proxy consumer qui va envoyer l'évènement vers le proxy supplier du client. On voit bien ici le croisement entre le serveur qui est un fournisseur et qui dialogue avec son proxy consumer et le client qui est un consommateur et qui dialogue avec son proxy supplier.
Le but de cette architecture est bien que clients et serveurs soient le plus découplés possible afin d'être le plus générique possible.

 

Avant toute chose, pour utiliser le service d'évènements, il est nécessaire de démarrer le service d'évènements.
Ce service se lance comme un serveur C++ traditionnel après avoir démarré le service de nommage.
La commande est la suivante :

CosEvent_Service -ORBInitRef NameService=corbaloc::pcdell:12345/NameService

 

Nous allons préparer un nouveau fichier IDL car nous allons envoyer un message particulier par l'intermédiaire du canal d'évènements. Ce message est une structure constituée de deux champs : une chaine de caractères et un flottant double précision. Il ne présente aucun interêt au niveau fonctionnel, nous l'avons choisi afin de bien montrer comment manipuler un type de données de type "structure" par l'intermédiaire du canal d'évènements.
Voici le contenu du fichier IDL (Convertisseur5.idl) correspondant au type de message que nous voulons envoyer grâce au service d'évènements :

module Convertisseur5
{
    struct Event
    {
        double valeur;
        string message;
    };

    interface Euro
    {
        attribute double taux;
        attribute string devise;
        double toEuro(in double devise);
        double toDevise(in double euro);
    };
};

 

L'interface PushSupplier est fournie par CORBA, le fichier IDL qui la décrit se trouve ici :
$(TAO_ROOT)\orbsvcs\orbsvcs\CosEventComm.idl.
Cette interface possède une unique méthode nommée disconnect_push_supplier() que le serveur doit implémenter :

void PushSupplierImpl::disconnect_push_supplier()
{
    cout << "Deconnexion de l'objet : " << m_name << endl;

    exception_message = "Erreur de deconnexion d'objet";
    PortableServer::ObjectId_var oid = PortableServer::string_to_ObjectId(m_name.c_str());
    try
    {
        m_POA->deactivate_object(oid);
    }
    catch (CORBA::Exception& ex)
    {
        ACE_ERROR((LM_ERROR, "PushSupplierImpl::disconnect_push_supplier - %s\n",         exception_message));
        ACE_PRINT_EXCEPTION(ex, "Exception");
    }
}

Le code source complet de cette implémentation se trouve dans les fichiers :
PushSupplierImpl.cpp et PushSupplierImpl.h

Les étapes suivantes décrivent l'attachement du serveur au canal d'évènements ainsi que l'envoi des messages du serveur vers ce canal :

  1. Connexion au service d'évènements :

CORBA::Object_var root_naming_context_obj =     orb->resolve_initial_references("NameService");

CosNaming::NamingContext_var root_naming_context =
    CosNaming::NamingContext::_narrow(root_naming_context_obj);

CosNaming::Name event_service_obj_name;
event_service_obj_name.length(1);
event_service_obj_name[0].id = CORBA::string_dup("CosEventService");
CORBA::Object_var event_service_obj = root_naming_context->resolve(event_service_obj_name);
CosEventChannelAdmin::EventChannel_var eventChannel =
    CosEventChannelAdmin::EventChannel::_narrow(event_service_obj);
cout << "Connexion au canal d'evenements effectuee" << endl;

 

  1. Création de l'objet PushSupplier :

PushSupplierImpl pushSupplier("PushSupplier", my_POA);

 

  1. Activation de l'objet PushSupplier dans le notre POA my_POA :

PortableServer::ObjectId_var oid = PortableServer::string_to_ObjectId("PushSupplier");
my_POA->activate_object_with_id(oid, &pushSupplier);

 

  1. Connexion de l'objet PushSupplier au proxy :

CosEventChannelAdmin::ProxyPushConsumer_ptr pushConsumer =     eventChannel->for_suppliers()->obtain_push_consumer();
pushConsumer->connect_push_supplier(pushSupplier._this());
cout << "PushSupplier connecte au proxy" << endl;

 

  1. Lancement de l'ORB TAO dans la méthode run_orb() threadée (asynchrone) :

ACE_Thread_Manager::instance()->spawn((ACE_THR_FUNC)run_orb, (void *)orb,
                                      THR_DETACHED | THR_NEW_LWP);

Ce lancement asynchrone permet de ne pas bloquer le serveur ici afin d'envoyer des messages qui seront saisis dans la console comme décrit dans l'étape suivante.

 

  1. Envoi de messages du serveur vers le client grâce au canal d'évènements :

On envoie au client un message (chaîne de caractères) saisie au clavier dans la console ainsi qu'un numéro de message afin de bien montrer que l'on peut envoyer n'importe quel type de message dans le canal d'évènements :

Convertisseur5::Event event_struct;
event_struct.valeur = 1;
while(true)
{
    CORBA::Any event;
    char message[100];

    strcpy(message, "");
    cout << "Entrer le message : ";
    cin >> message;

    // Un CTRL+C fait que message = ""
    // On met alors le thread courant en attente car un signal d'interruption (SIGINT)
    // va etre lance et il faut laisser la methode deconnexion() s'executer
    if (strcmp(message, "") == 0)
    {
        ACE_Thread_Manager::instance()->wait();
    }

    event_struct.message = CORBA::string_dup(message);

    // Noter l'utilisation geniale de l'operateur <<= qui permet de convertir n'importe
    // quel type vers le type CORBA::Any
    //event <<= message; // Envoi d'une string uniquement
    event <<= event_struct; // Envoi d'une structure
    pushConsumer->push(event);

    cout << "Evenement envoye au client" << endl;
    event_struct.valeur++;
}

Le code source complet du serveur se trouve dans le fichier Serveur.cpp.

 

L'interface PushConsumer est fourni par CORBA, le fichier IDL qui la décrit se trouve ici :
$(TAO_ROOT)\orbsvcs\orbsvcs\CosEventComm.idl.
Cette interface possède les deux méthodes push() et disconnect_push_consummer() que le client doit implémenter :

import org.omg.CORBA.*;
import org.omg.CosEventComm.*;
import org.omg.PortableServer.*;

import Convertisseur5.*;

public class PushConsumerImpl extends PushConsumerPOA {

    private String name;
    private POA poa;

    public PushConsumerImpl(String name, POA poa) {
        this.name = name;
        this.poa = poa;
    }

    public void push(Any event) throws Disconnected {
        if (event.type().kind() == TCKind.tk_string) {
            System.out.println("Evenement de type string recu : "
                                + event.extract_string());
        }
        else {
            System.out.println("Evenement de type Event recu : ");
            System.out.println("Valeur = " +
            EventHelper.extract(event).valeur);
            System.out.println("Message = " +
            EventHelper.extract(event).message);
        }
    }

    public void disconnect_push_consumer() {
        System.out.println("Deconnexion de l'objet : " + name);
        try {
            poa.deactivate_object(name.getBytes());
        }
        catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

Ce code source se trouve dans le fichier PushConsumerImpl.java.

Les étapes suivantes décrivent l'attachement du client au canal d'évènements :

  1. Connexion au service d'évènements :

NameComponent[] pathCanalEvents = {
    new NameComponent("CosEventService", "")
};
org.omg.CORBA.Object eventServiceObj = rootNamingContext.resolve(pathCanalEvents);

EventChannel eventChannel = EventChannelHelper.narrow(eventServiceObj);
System.out.println("Connexion au canal d'evenements effectuee");

 

  1. Création de l'objet PushConsumer :

PushConsumerImpl pushConsumer = new PushConsumerImpl("PushConsumer", myPOA);

 

  1. Activation de l'objet PushConsumer dans le notre POA myPOA :

String pushConsumerName = "PushConsumer";
myPOA.activate_object_with_id(pushConsumerName.getBytes(), pushConsumer);

 

  1. Connexion de l'objet PushConsumer au proxy

ProxyPushSupplier pushSupplier pushSupplier =     eventChannel.for_consumers().obtain_push_supplier();
pushSupplier.connect_push_consumer(pushConsumer._this());

System.out.println("PushConsummer connecte au proxy");

Le code source complet du client se trouve dans le fichier Client.java.

 

L'implémentation de notre client/serveur utilisant le service d'évènements afin que le serveur puisse également envoyer des messages au client par l'intermédiaire du canal d'évènements est à présent terminée.
Le test de l'application se déroule de la même façon que pour l'exemple de l'architecture mixte que nous avons étudiée dans le paragraphe précédent. La seule différence notable est qu'il ne faut surtout pas oublier de démarrer le service d'évènements juste après le service de nommage et juste avant le serveur C++ et le client Java.
Le serveur peut alors envoyer des messages au client par l'intermédiaire de la console, ceux-ci seront affichés dans la console Java du client.

Remarque :

On constatera que si l'on fait fonctionner le serveur sans démarrer le client, les messages vont être envoyés mais dans le vide, sans que personne ne les recoivent. En revanche, il n'y a pas d'erreurs ni d'exceptions au niveau du serveur, cela prouve bien que client et serveur sont bien isolés l'un de l'autre. Bien sûr, dès que l'on démarre le client et que le serveur envoie un nouveau message, celui-ci sera aussitôt reçu par le client.

 

L'archive service_evenements.zip contient tous les fichiers source de cet exemple.

 

Retour haut de page