XML-RPC entre C++ et Ruby

Comme son nom l'indique, XML-RPC permet d'appeler des fonctions à distance (Remote Procedure Call) en sérialisant paramètres et résultats en XML. Il a l'avantage d'être très simple à mettre en place, multiplateforme et peu coûteux en ressources ce qui en fait un bon candidat pour la communication avec des systèmes embarqués.

Je vous propose une démonstration de communication XML-RPC entre un serveur en Ruby qui lit les données d'un annuaire depuis une base SQLite et un petit programme C++ qui permet de rechercher des personnes.

Avertissement

Les exemples présentés ici sont condensés pour des raisons pratiques. La définition des classes est faite au milieu du code seulement pour faciliter la lecture. La gestion des erreurs a été omise.

Serveur (Ruby)

Explications

Commençons par créer la base SQLite avec 2 entrées :

$ sqlite3 annuaire.db
SQLite version 3.6.16
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> CREATE TABLE personne (id integer primary key, nom varchar(255), prenom varchar(255));
sqlite> insert into personne (id,nom,prenom) values (1,"Onyme","Anne");
sqlite> insert into personne (id,nom,prenom) values (2,"Dubois","Pierre");

Passons à la partie serveur. Nous définissons la classe Personne en Ruby très simplement :

# Classe personne
class Personne
        attr_accessor :id, :nom, :prenom

        def initialize(id, nom, prenom)
                @id = id
                @nom = nom
                @prenom = prenom
        end

        def to_hash
                { "id" => @id, "nom" => @nom, "prenom" => @prenom }
        end
end

Un constructeur initialise toutes les variables d'instance, et une méthode to_hash sera utilisée pour la sérialisation. Nous utilisons les modules sqlite3 pour l'accès à la base de données et xmlrpc/server pour le serveur XML-RPC.

Nous ajoutons au serveur HTTP un handler qui se contente de faire une sélection dans la base SQLite, et de retourner une table de hachage (hash) contenant les couples clé/valeur des champs de la façon suivante :

# Ajout du handler de requetes "recherche"
s.add_handler("personne.recherche") do |id|
        p = nil
        db.execute( "select * from personne where id = #{id}") do |row|
                p = Personne.new(Integer(row[0]), row[1].to_s, row[2].to_s)
        end
        { "res" => p.to_hash }
end

C'est le module xmlrpc/server qui se chargera d'associer les bons types aux champs retournés, et de produire le code XML correspondant.

Listing complet

Ainsi, le code complet de notre serveur XML-RPC en Ruby est le suivant :

#!/usr/bin/ruby

# Imports
require 'xmlrpc/server'
require 'sqlite3'

# Classe personne
class Personne
        attr_accessor :id, :nom, :prenom

        def initialize(id, nom, prenom)
                @id = id
                @nom = nom
                @prenom = prenom
        end

        def to_hash
                { "id" => @id, "nom" => @nom, "prenom" => @prenom }
        end
end

# Connexion a la base et serveur HTTP
db = SQLite3::Database.new('annuaire.db')
s = XMLRPC::Server.new(1080, '0.0.0.0')

# Ajout du handler de requetes "recherche"
s.add_handler("personne.recherche") do |id|
        p = nil
        db.execute( "select * from personne where id = #{id}") do |row|
                p = Personne.new(Integer(row[0]), row[1].to_s, row[2].to_s)
        end
        { "res" => p.to_hash }
end

# Laisse la main au serveur HTTP
s.serve

Nous pouvons le lancer avec :

$ ./server.rb
[2010-06-09 15:21:08] INFO  WEBrick 1.3.1
[2010-06-09 15:21:08] INFO  ruby 1.8.6 (2009-06-08) [x86_64-linux]
[2010-06-09 15:21:08] INFO  WEBrick::HTTPServer#start: pid=7416 port=1080

Client (C++)

Explications

Nous utilisons la bibliothèque XmlRpcCpp. Comme dans le code Ruby, définissons notre classe Personne :

/**
 * Classe personne.
 */
class personne
{
private:
        int m_id;
        string m_nom;
        string m_prenom;

public:
        personne(int id, string nom, string prenom) : m_id(id), m_nom(nom), m_prenom(prenom) {};
        string description() {
                stringstream out;
                out <<  m_prenom << " " << m_nom << " (" << m_id << ")";
                return out.str();
        }
};

De nouveau un constructeur initialise toutes les variables d'instance, et une méthode description renvoie une chaîne décrivant l'instance qui sera utilisée pour l'affichage.

Nous commençons par initialiser l'API avec des informations sur le serveur, comme l'URL :

#define NAME       "Exemple d'appel XML-RPC entre C++ et Ruby"
#define VERSION    "0.1"
#define SERVER_URL "http://127.0.0.1:1080/RPC2"

        XmlRpcClient::Initialize(NAME, VERSION);
        XmlRpcClient server(SERVER_URL);

Pour réaliser l'appel XML-RPC, nous construisons un tableau de paramètre ne contenant qu'un nombre entier qui aura été passé comme paramètre du programme. Il correspond à l'identifiant de l'entrée recherchée :

        XmlRpcValue param_array = XmlRpcValue::makeArray();
        param_array.arrayAppendItem(XmlRpcValue::makeInt(atoi(argv[1])));
        XmlRpcValue result = server.call("personne.recherche", param_array);

La chaîne personne.recherche est le nom lié au handler que nous avons défini sur le serveur. Après l'appel, nous désérialisons manuellement la structure XML-RPC qui nous a été retournée par l'appel synchrone au serveur :

        XmlRpcValue structXmlRpc = result.structGetValue("res").getStruct();
        personne p(
                structXmlRpc.structGetValue("id").getInt(),
                structXmlRpc.structGetValue("nom").getString(),
                structXmlRpc.structGetValue("prenom").getString());

Nous avons maintenant une instance de notre classe Personne sur le client qui est une image de celle que nous avions sur le serveur. Ceci est exactement le but recherché. Nous sommes parvenu à nous abstraire complètement de l'architecture et du langage utilisé sur le serveur mais à récupérer des données structurées. Nous pouvons désormais manipuler l'instance comme si elle avait été créée localement, par exemple pour en afficher la description :

        cout << "Résultat : " << p.description() << endl;

Listing complet

Le code C++ du client est placé dans un seul fichier que voici :

#include <XmlRpcCpp.h>

#include <iostream>
#include <sstream>

#define NAME       "Exemple d'appel XML-RPC entre C++ et Ruby"
#define VERSION    "0.1"
#define SERVER_URL "http://127.0.0.1:1080/RPC2"

using namespace std;

/**
 * Classe personne.
 */
class personne
{
private:
        int m_id;
        string m_nom;
        string m_prenom;

public:
        personne(int id, string nom, string prenom) : m_id(id), m_nom(nom), m_prenom(prenom) {};
        string description() {
                stringstream out;
                out <<  m_prenom << " " << m_nom << " (" << m_id << ")";
                return out.str();
        }
};

int main(int argc, char *argv[]) {
        /**
         * Initialisation XML-RPC.
         */
        XmlRpcClient::Initialize(NAME, VERSION);
        XmlRpcClient server(SERVER_URL);

        /**
         * Appel RPC avec un tableau de paramètres.
         */
        XmlRpcValue param_array = XmlRpcValue::makeArray();
        param_array.arrayAppendItem(XmlRpcValue::makeInt(atoi(argv[1])));
        XmlRpcValue result = server.call("personne.recherche", param_array);

        /**
         * Conversion de la structure vers une instance de classe Personne.
         */
        XmlRpcValue structXmlRpc = result.structGetValue("res").getStruct();
        personne p(
                structXmlRpc.structGetValue("id").getInt(),
                structXmlRpc.structGetValue("nom").getString(),
                structXmlRpc.structGetValue("prenom").getString());

        /**
         * Affichage du résultat.
         */
        cout << "Résultat : " << p.description() << endl;

        return 0;

}

Compilation et exécution

Le programme client peut être compilé avec :

g++ `xmlrpc-c-config c++ libwww-client --cflags` `xmlrpc-c-config c++ libwww-client --libs` client.cpp -o client

Pour l'exécuter, il faut passer l'identifiant de la personne recherchée en paramètre :

$ ./client 1
Résultat : Anne Onyme (1)
$ ./client 2
Résultat : Pierre Dubois (2)

Remarques sur cette architecture

L'exécution du serveur en Ruby ne prend que quelques centaines de kilo octets en mémoire vive. Il est donc possible de le faire tourner sur des systèmes embarqués aux ressources limités, d'autant plus qu'il existe des interpréteurs Ruby sous Linux pour beaucoup d'architectures, y compris MIPS, ARM. Il existe aussi un interpréteur Ruby pour téléphones Symbian.

Il est donc possible d'offrir à moindre coût une interface RPC presque universelle à des données d'un système embarqués, mais aussi à des capteurs et des effecteurs.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.