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.