Devoir Surveillé - Systèmes Répartis et Applications
Exercice 1 : Questions diverses Question 1 - Vrai ou Faux Voici les évaluations des différentes affirmations, avec leurs justifications. a) Le mécanisme de rpc peut appeler une procédure d’un processus localisé sur une autre machine ? Vrai. C'est l'essence même du RPC (Remote Procedure Call) : permettre l'exécution de code sur un système distant de manière transparente pour le développeur.
D'après le document Devoir Surveillé - Systèmes Répartis et Applications
Cet article a été rédigé automatiquement à partir du document source, puis vérifié avant publication.

Document source
Programming, Distributed Systems · PDF · 10 pages · 2014
Afficher l'aperçu du document
Exercice 1 : Questions diverses
Question 1 - Vrai ou Faux
Voici les évaluations des différentes affirmations, avec leurs justifications.
a) Le mécanisme de rpc peut appeler une procédure d’un processus localisé sur une autre machine ? Vrai. C'est l'essence même du RPC (Remote Procedure Call) : permettre l'exécution de code sur un système distant de manière transparente pour le développeur.
b) Un programme utilisateur Sun RPC peut avoir n’importe quel numéro dans la plage 0x10000000-0x3fffffff. Faux. La plage 0x10000000 à 0x1fffffff n'est pas librement utilisable par n'importe quel utilisateur. La plage réservée et définie par Sun pour les programmes utilisateurs ou développés localement est 0x20000000 à 0x3fffffff.
c) Sun RPC permet au développeur de spécifier le nom d’une interface.
Vrai. Dans le fichier .x, le développeur déclare un nom de programme (par exemple RANDOM_PROG) qui regroupe les procédures, agissant comme le nom de l'interface.
d) L’utilitaire rpcgen génère une définition d’interface d’accès à une procédure distante.
Faux. rpcgen ne génère pas la définition (qui est le fichier .x écrit par le développeur). Il génère le code en C correspondant : le squelette (skeleton) pour le serveur, la souche (stub) pour le client, les routines de sérialisation XDR et le fichier d'en-tête (header .h).
e) Un objet sérialisable peut être passé comme argument ou une valeur de retour à un objet local. Vrai. La sérialisation permet de transformer l'état d'un objet en un flux d'octets. Il peut donc être passé par valeur sur le réseau (comme argument ou retour).
f) Le rmiregistry est toujours sur le port 1099. Faux. Le port 1099 est le port par défaut du service de nommage RMI. Cependant, il peut être explicitement modifié lors du lancement du registre ou dans le code via l'API.
g) Un « remote object reference » est un identifiant unique d’objet dans les systèmes distribués. Vrai. La référence d'objet distant est un identifiant globalement unique (contenant l'adresse IP, le port et un identifiant interne) permettant de localiser et d'invoquer l'objet sur le réseau.
Question 2 - Objectifs d'un middleware
L'utilisation d'un intergiciel (middleware) dans un système réparti possède plusieurs objectifs majeurs. En voici trois principaux :
- Masquer l'hétérogénéité des systèmes sous-jacents (réseaux, matériels, systèmes d'exploitation, langages de programmation).
- Faciliter la conception, le développement et le déploiement des applications en fournissant des API de haut niveau (comme RPC ou RMI).
- Fournir des services communs et standardisés (service de nommage, transactions, sécurité, concurrence).
Question 3 - Différences fondamentales
a) Serveur persistant vs Serveur stateful : La différence réside dans la nature de ce qui est conservé. Un serveur persistant stocke les données qu'il manipule de façon permanente (sur disque) afin qu'elles survivent aux pannes du serveur. Un serveur stateful (avec état) conserve en mémoire le contexte ou l'état spécifique d'un client au fil de ses requêtes successives lors d'une session.
b) portmapper vs rpcbind :
rpcbind est le successeur évolué du portmapper. Bien que tous deux fassent le lien entre un numéro de programme RPC et un port réseau, rpcbind supporte des protocoles de transport plus modernes, notamment IPv6 et TI-RPC (Transport-Independent RPC).
c) UnicastRemoteObject vs Activatable :
L'extension de la classe UnicastRemoteObject permet d'exporter et d'activer explicitement un objet distant dès sa création ; il reste continuellement actif en mémoire. L'utilisation d'Activatable permet une activation implicite et à la demande : l'objet distant n'est chargé en mémoire par le démon RMI (rmid) que lorsqu'un client l'invoque réellement.
d) Classpath vs Codebase :
Le Classpath est une variable d'environnement utilisée pour localiser des classes Java localement sur le disque de la machine qui exécute la JVM. Le Codebase est une propriété utilisée en RMI pour spécifier une ou plusieurs URL depuis lesquelles les JVM distantes peuvent télécharger dynamiquement des classes (comme les stubs) qui ne figurent pas dans leur Classpath local.
Question 4 - Analyse de la commande rpcinfo
$ rpcinfo –u remoteHost 53687093 2
Cette commande tente d'effectuer un appel RPC (souvent vu comme un "ping" RPC) vers la machine distante nommée remoteHost.
-uspécifie que le protocole de transport à tester est UDP.53687093est le numéro du programme RPC ciblé.2est le numéro de version de ce programme.
Elle permet de vérifier que le réseau fonctionne, que le portmapper distant répond, et que la version 2 de ce programme spécifique est bien enregistrée et fonctionnelle sur la machine.
Question 5 - Utilité de la version d'un programme rpc
Gérer un numéro de version permet de faire évoluer un service sans casser la compatibilité avec les clients existants. Un même serveur peut enregistrer et offrir simultanément la version 1 (pour les anciens clients) et la version 2 (pour les nouveaux clients disposant de nouvelles fonctionnalités), assurant ainsi l'indépendance de déploiement entre le client et le serveur.
Exercice 2 : Programmation par RPC
Question 1 - Fichier de spécification Rand.x
Voici le fichier RPCL. Il est crucial d'utiliser le mot-clé struct et de définir un typedef pour simplifier l'usage en C.
/* Fichier Rand.x */
struct par1 {
unsigned inf;
unsigned sup;
};
typedef struct par1 Par1;
program RANDOM_PROG {
version VERS_UN {
void RAND_NULL(void) = 0;
unsigned INIT_2RAND(Par1) = 1;
unsigned GET_NEXT(unsigned) = 2;
} = 1;
} = 0x23456781;
Question 2 - Code du serveur Rand_Server.c
Attention d'implémentation : Le document source propose l'instruction result = *argp ++; pour la fonction get_next. En langage C, à cause de la priorité des opérateurs, *argp++ déréférence le pointeur et l'évalue, puis incrémente l'adresse du pointeur, ce qui ne modifie pas la valeur et renvoie l'ancienne. Le code corrigé ci-dessous utilise *argp + 1 pour bien incrémenter la valeur elle-même.
#include "Rand.h"
#include <stdio.h>
#include <stdlib.h>
void * rand_null_1_svc(void *argp, struct svc_req *rqstp) {
static char result;
printf("Success Ping\n");
fflush(stdout);
return (void *) &result;
}
unsigned * init_2rand_1_svc(Par1 *argp, struct svc_req *rqstp) {
static unsigned result;
/* On suppose que argp->inf < argp->sup */
result = (rand() % (argp->sup - argp->inf)) + argp->inf;
return &result;
}
unsigned * get_next_1_svc(unsigned *argp, struct svc_req *rqstp) {
static unsigned result;
/* Correction de l'incrémentation : on augmente la valeur déréférencée */
result = *argp + 1;
return &result;
}
Question 3 - Chaîne de production et vérification
Pour compiler et produire les exécutables, on lance les commandes suivantes dans l'ordre :
# Génération des stubs, skeletons et de l'en-tête
rpcgen Rand.x
# Compilation du code côté serveur
gcc -o serveur Rand_Server.c Rand_svc.c Rand_xdr.c
# Compilation du code côté client
gcc -o client Rand_Client.c Rand_clnt.c Rand_xdr.c
Pour vérifier son fonctionnement localement :
- On démarre le serveur en arrière-plan :
./serveur & - On vérifie son enregistrement auprès du portmapper :
rpcinfo -p localhost(on doit y voir le numéro 0x23456781). - On vérifie que la procédure répond correctement avec le ping rpc :
rpcinfo -u localhost 0x23456781 1(remplacer-upar-tpour tester TCP).
Question 4 - Connexion du client et appel distant
Les modules C générés par rpcgen nécessaires à la compilation du client sont : Rand_Client.c (votre code client), Rand_clnt.c (les stubs clients) et Rand_xdr.c (les filtres de conversion de données).
Voici la logique de connexion et d'appel (les fonctions générées par rpcgen s'attendent toujours à recevoir des pointeurs et renvoient des pointeurs) :
#include "Rand.h"
#include <stdio.h>
int main(int argc, char *argv[]) {
CLIENT *clnt;
unsigned arg = 0x3fff;
unsigned *result;
char *host = "localhost"; /* ou l'adresse IP du serveur */
/* Création du handle de connexion */
clnt = clnt_create(host, RANDOM_PROG, VERS_UN, "tcp");
if (clnt == NULL) {
clnt_pcreateerror(host);
return 1;
}
/* Appel distant. On passe l'adresse de l'argument. */
result = get_next_1(&arg, clnt);
if (result == (unsigned *) NULL) {
clnt_perror(clnt, "Appel distant a echoue");
} else {
printf("Le résultat de get_next(0x3fff) est : %x\n", *result);
}
clnt_destroy(clnt);
return 0;
}
Question 5 - Modification du serveur (Nouvelle version)
a) Nouvelle spécification rand.x
Il faut ajouter une nouvelle structure Par2 pour gérer les deux paramètres (paramètre de base et le pas) et définir la version 2 en maintenant la version 1 pour la rétrocompatibilité.
struct par1 {
unsigned inf;
unsigned sup;
};
typedef struct par1 Par1;
struct par2 {
unsigned param;
unsigned pas;
};
typedef struct par2 Par2;
program RANDOM_PROG {
version VERS_UN {
void RAND_NULL(void) = 0;
unsigned INIT_2RAND(Par1) = 1;
unsigned GET_NEXT(unsigned) = 2;
} = 1;
version VERS_DEUX {
void RAND_NULL(void) = 0;
unsigned INIT_2RAND(Par1) = 1;
unsigned GET_NEXT(Par2) = 2;
} = 2;
} = 0x23456781;
b) Fonctions implémentées et code client
Dans le fichier serveur (Rand_Server.c), de nouvelles fonctions vont devoir être implémentées. Leurs prototypes générés par rpcgen seront :
void * rand_null_2_svc(void *, struct svc_req *);
unsigned * init_2rand_2_svc(Par1 *, struct svc_req *);
unsigned * get_next_2_svc(Par2 *, struct svc_req *);
Pour le client souhaitant exécuter get_next(init_2rand(1,5000), 5), l'enchaînement se fait de manière séquentielle car l'API générée par RPC manipule des pointeurs.
Correction de la méthode : La correction du document initial suggère un appel imbriqué par valeur : nb.param = init_2rand_2(borne, clnt);. Ceci déclenchera des erreurs de compilation en C car init_2rand_2 prend l'adresse de borne et renvoie un pointeur unsigned *. Voici le code C valide et sécurisé :
CLIENT *clnt;
Par1 borne;
Par2 nb;
unsigned *res_init;
unsigned *res_final;
/* 1. Connexion sur la version 2 */
clnt = clnt_create(host, RANDOM_PROG, VERS_DEUX, "tcp");
/* 2. Préparation et appel de init_2rand */
borne.inf = 1;
borne.sup = 5000;
res_init = init_2rand_2(&borne, clnt);
if (res_init != NULL) {
/* 3. Préparation et appel de get_next avec le résultat précédent */
nb.param = *res_init;
nb.pas = 5;
res_final = get_next_2(&nb, clnt);
if (res_final != NULL) {
printf("Résultat final : %u\n", *res_final);
}
}
Question 6 - Déconnexion du client
La fonction à appeler pour libérer les ressources réseau allouées au handle client est :
clnt_destroy(clnt);
Exercice 3 : Programmation RMI
Question 1 - Complétion des interfaces et classes
Pour que l'interface et la classe fonctionnent dans le cadre Java RMI, avec la prise en charge de la libération de l'objet temporaire (DGC - Distributed Garbage Collector), les codes modifiés sont :
L'interface IBonjour :
Elle doit hériter de java.rmi.Remote et chaque méthode doit déclarer jeter l'exception de communication java.rmi.RemoteException.
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface IBonjour extends Remote {
public void bonjour() throws RemoteException;
}
La classe Bonjour :
Elle doit implémenter l'interface RMI java.rmi.server.Unreferenced pour que le serveur soit notifié (via la méthode unreferenced()) lorsque plus aucun client ne maintient de référence vers cet objet.
import java.rmi.RemoteException;
import java.rmi.server.Unreferenced;
public class Bonjour implements IBonjour, Unreferenced {
public Bonjour() { }
public void bonjour() throws RemoteException {
System.out.println("Bonjour.bonjour()");
}
public void unreferenced() {
try {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
tg.destroy();
/* Ou tout autre code pour arrêter le traitement temporaire */
} catch (Exception e) {
System.out.println(e.toString());
}
}
}
Question 2 - Rendre Hello transférable sur le réseau
Puisque Hello n'est pas un objet distant (il n'implémente pas Remote), il doit être transféré par valeur lors d'un appel réseau (passage d'une copie). Pour cela, la classe doit implémenter l'interface marqueur java.io.Serializable.
import java.io.Serializable;
public class Hello implements Serializable {
public void hello() {
System.out.println("Hello.hello()");
}
}
Question 3 - Exécution de direHello (ou hello)
L'énoncé demande ce qu'il se passe sur l'appel s.newHello().direHello(). Il y a ici une incohérence dans l'énoncé source relevée par la correction : newHello() retourne un objet de type Hello, lequel possède la méthode hello(), et non pas direHello(). direHello(Hello h) est une méthode du serveur.
Scénario A (Si le client exécute s.newHello().hello()) :
L'appel newHello() retourne un objet Hello par valeur (il est sérialisé par le serveur puis désérialisé chez le client). L'appel de .hello() s'exécute donc par la JVM du client sur la copie locale fraîchement reçue.
Scénario B (Si le client exécute s.direHello(s.newHello())) :
Dans ce cas, le client reçoit la copie locale via newHello() (passée par valeur). Ensuite, il appelle la méthode distante direHello(Hello h) de l'objet serveur s, en lui passant cette copie. La méthode direHello sera bien exécutée par la JVM du Serveur, en utilisant l'objet qui lui est passé en paramètre.
Question 4 - Exécution de bonjour
L'expression s.newBonjour().bonjour() combine deux étapes :
newBonjour()retourne une référence d'objet distant (un stub pourIBonjour) car l'interface implémenteRemote. L'objet n'est donc pas copié, il reste sur le serveur.- L'appel à
.bonjour()s'effectue sur ce stub. L'appel transite alors par le réseau. Par conséquent, la méthodebonjour()s'exécute sur la JVM du Serveur (là où vit l'objetBonjour).
Question 5 - Code du client
Voici le code du client RMI complet qui effectue la recherche dans l'annuaire (le registry) puis appelle les méthodes demandées.
import java.rmi.Naming;
public class BienvenueClient {
public static void main(String[] args) {
try {
/* On récupère la référence de l'objet Serveur */
IBienvenue obj = (IBienvenue) Naming.lookup("rmi://localhost:1099/MonBienvenue");
/* Appel sur un objet distant renvoyé sous forme de stub */
obj.newBonjour().bonjour();
/* Appel local sur un objet renvoyé par copie (sérialisé) */
obj.newHello().hello();
} catch (Exception e) {
System.out.println("Erreur BienvenueClient : " + e.getMessage());
e.printStackTrace();
}
}
}
Question 6 - Classes nécessaires dans le CLASSPATH du client
Si le téléchargement dynamique du code (codebase) n'est pas utilisé, le client doit posséder physiquement toutes les classes requises en local dans son CLASSPATH :
BienvenueClient.class: C'est le bytecode du client lui-même, essentiel pour lancer le programme.IBienvenue.classetBienvenue_stub.class: Requis pour caster le résultat dulookupet pour effectuer les invocations réseau vers l'objet serveur principal (newBonjour,newHello).IBonjour.classetBonjour_stub.class: L'interface et le stub sont nécessaires carnewBonjour()renvoie une instance d'un objet RMI et le client doit interagir avec via son stub local.Hello.class: Le code complet de la classe est requis pour que le mécanisme de désérialisation de Java puisse reconstruire la copie locale et que la JVM puisse invoquer la méthode.hello().
Méthode
Face à une épreuve de systèmes répartis qui aborde à la fois le RPC C classique et le RMI Java, il est essentiel d'avoir en tête les mécanismes sous-jacents de mémoire et de transmission :
- En C (Sun RPC) : Tous les arguments et retours réseau sont encapsulés sous forme de pointeurs par les stubs générés par
rpcgen(T * func(T *, CLIENT *)). Ne perdez jamais de vue que le résultat est un pointeur statique alloué côté stub client. Les appels en cascadef1(f2())échoueront toujours à la compilation ; vous devez stocker le pointeur intermédiaire. N'oubliez pas non plus la priorité des opérateurs (*argp++vs*argp + 1). - En Java (RMI) : La ligne de partage réseau se décide via vos interfaces. Si un type implémente
java.rmi.Remote, il transite par référence (passage d'un stub ; les appels s'exécutent sur le serveur). S'il implémentejava.io.Serializablemais pasRemote, il transite par valeur (il est cloné sur le réseau ; l'exécution se fait localement chez le réceptionnaire). Bien identifier cette nature (Question 3 vs Question 4) garantit de toujours savoir quelle JVM exécute véritablement le code.
Commentaires
Aucun commentaire pour le moment. Posez la première question.