QxOrm - tutoriel qxClientServer

Lionel Marty

QxOrm est une bibliothèque C++ de gestion de données (Mapping Objet Relationnel ou ORM) basée sur les frameworks Qt et boost.
Le tutoriel qxClientServer présente le module QxService de la bibliothèque QxOrm avec la création d'un serveur d'applications en Qt/C++.

Cet article décrit la création de trois projets :

- une interface graphique pour la partie serveur ;

- une interface graphique pour la partie cliente avec des appels aux services proposés par le serveur ;

- une couche service comprenant un exemple rapide d'utilisation plus un exemple avec une classe persistante.

Pour plus d'informations sur la bibliothèque QxOrm, rendez-vous sur le site officiel.

Commentez

- Titre : QxOrm - tutoriel qxClientServer

- Auteur : Lionel Marty

- Parution : 29 août 2011

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2011 Lionel Marty. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Présentation du projet qxClientServer

- II. Création de l'interface serveur : qxServer

- II-A. Description du fichier main_dlg.h

- II-B. Description du fichier main_dlg.cpp, méthode init()

- II-C. Description du fichier main_dlg.cpp, méthode loadServices()

- II-D. Description du fichier main_dlg.cpp, méthode onClickStartStop()

- II-E. Description du fichier main_dlg.cpp, méthodes onError() et onTransactionFinished()

- II-F. Résultat obtenu pour le projet qxServer

- III. Création de la couche service : qxService

- III-A. Écriture du premier service : récupérer la date et l'heure courantes du serveur

- III-B. Description du fichier server_infos.h

- III-C. Description du fichier server_infos.cpp

- III-D. Écriture du second service : opérations avec une classe persistante

- IV. Création de l'interface cliente : qxClient

- IV-A. Description de la méthode onClickBtnDateTime()

- IV-B. Description de la méthode onClickBtnDateTimeAsync()

- IV-C. Description de la méthode onClickBtnAddUser()

- IV-D. Description de la méthode onClickBtnGetAllUsers()

- V. Remerciements

I - Présentation du projet qxClientServer

QxOrm fournit les fonctionnalités suivantes à partir d'une simple fonction de paramétrage (similaire à un fichier de mapping XML pour Hibernate en Java) :

- persistance : basé sur le module QtSql de Qt, communication avec de nombreuses bases de données (avec support des relations 1-1, 1-n, n-1 et n-n) ;

- sérialisation : basé sur le module boost::serialization de boost, flux de données binaire et XML ;

- réflexion (ou introspection) : accès aux classes, propriétés et méthodes par chaînes de caractères (gestion dynamique des objets).

Le tutoriel qxClientServer a pour objectif d'expliquer le fonctionnement du module QxService de la bibliothèque QxOrm. Le module QxService permet de créer rapidement un serveur d'applications C++ performant (notion de services avec demande du client et réponse du serveur). Les sources du projet qxClientServer sont disponibles dans le dossier ./test/qxClientServer/ de la distribution de QxOrm. Il est conseillé d'avoir lu le tutoriel qxBlog avant de lire cet article, notamment tout ce qui concerne la fonction de mapping de QxOrm : void qx::register_class(...).

Le résultat final de ce tutoriel comporte deux exécutables et une couche service :

- qxServer : serveur d'applications C++ avec une interface utilisateur pour paramétrer le serveur et un champ pour afficher la dernière transaction effectuée entre le client et le serveur ;

- qxClient : interface utilisateur contenant plusieurs boutons pour exécuter différentes requêtes au serveur ;

- qxService : couche service, le serveur et le client partagent cette même couche pour transférer les données et appeler les services.

Remarque : pour plus de détails sur la notion de socket, de thread et de réseau, le site de Qt propose des tutoriels sur l'utilisation du module QtNetwork :

- Fortune Client ;

- Blocking Fortune Client ;

- Fortune Server ;

- Threaded Fortune Server.

II - Création de l'interface serveur : qxServer

Le projet qxServer contient une seule fenêtre : l'interface utilisateur a été réalisée avec l'outil Qt Designer proposé par la bibliothèque Qt. Cette interface a pour seul objectif d'afficher à l'utilisateur la dernière transaction client-serveur et de configurer certains paramètres du serveur. Pour une utilisation réelle (logiciel de production), il est conseillé de proposer un système de log plutôt qu'un affichage à l'utilisateur. Une interface la plus minimaliste possible (voire aucune interface) est de manière générale la solution optimale pour un serveur d'applications. Les fichiers main_dlg.h et main_dlg.cpp correspondent au code C++ de l'interface du projet qxServer.

II-A - Description du fichier main_dlg.h

fichier main_dlg.h

#ifndef _QX_SERVER_MAIN_DLG_H_
#define _QX_SERVER_MAIN_DLG_H_

#include "../qt/ui/include/ui_qxServer.h"

class main_dlg : public QWidget, private Ui::dlgServer
{ Q_OBJECT

private:

qx::service::QxThreadPool_ptr m_pThreadPool; // Liste de threads pour recevoir les requêtes des clients

public:

main_dlg(QWidget * parent = NULL) : QWidget(parent), Ui::dlgServer() { main_dlg::init(); }
virtual ~main_dlg() { ; }

private:

void init();
void loadServices();

private Q_SLOTS:

void onClickStartStop();
void onCboIndexChanged(int index);
void onError(const QString & err, qx::service::QxTransaction_ptr transaction);
void onServerIsRunning(bool bIsRunning, qx::service::QxServer * pServer);
void onTransactionFinished(qx::service::QxTransaction_ptr transaction);

};

#endif // _QX_SERVER_MAIN_DLG_H_

La variable m_pThreadPool de type qx::service::QxThreadPool_ptr contient toute la logique du serveur d'applications. Cette logique est gérée de manière automatique par la bibliothèque QxOrm. La méthode init() permet d'initialiser les paramètres par défaut du serveur, de connecter les événements (mécanisme de signaux et de slots de Qt) et de lancer automatiquement le serveur. Nous allons voir tout ceci plus en détails avec l'implémentation des méthodes dans le fichier main_dlg.cpp.

II-B - Description du fichier main_dlg.cpp, méthode init()

fichier main_dlg.cpp, méthode init()

void main_dlg::init()
{
setupUi(this);

QObject::connect(btnStartStop, SIGNAL(clicked()), this, SLOT(onClickStartStop()));
QObject::connect(cboSerializationType, SIGNAL(currentIndexChanged(int)), this, SLOT(onCboIndexChanged(int)));

cboSerializationType->addItem("0- serialization_binary", QVariant((int)qx::service::QxConnect::serialization_binary));
cboSerializationType->addItem("1- serialization_xml", QVariant((int)qx::service::QxConnect::serialization_xml));
cboSerializationType->addItem("2- serialization_text", QVariant((int)qx::service::QxConnect::serialization_text));
cboSerializationType->addItem("3- serialization_portable_binary", QVariant((int)qx::service::QxConnect::serialization_portable_binary));
cboSerializationType->addItem("4- serialization_wide_binary", QVariant((int)qx::service::QxConnect::serialization_wide_binary));
cboSerializationType->addItem("5- serialization_wide_xml", QVariant((int)qx::service::QxConnect::serialization_wide_xml));
cboSerializationType->addItem("6- serialization_wide_text", QVariant((int)qx::service::QxConnect::serialization_wide_text));
cboSerializationType->addItem("7- serialization_polymorphic_binary", QVariant((int)qx::service::QxConnect::serialization_polymorphic_binary));
cboSerializationType->addItem("8- serialization_polymorphic_xml", QVariant((int)qx::service::QxConnect::serialization_polymorphic_xml));
cboSerializationType->addItem("9- serialization_polymorphic_text", QVariant((int)qx::service::QxConnect::serialization_polymorphic_text));
cboSerializationType->setCurrentIndex(cboSerializationType->findData(QVariant((int)qx::service::QxConnect::getSingleton()->getSerializationType())));

spinPortNumber->setValue(7694);
spinThreadCount->setValue(qx::service::QxConnect::getSingleton()->getThreadCount());
onServerIsRunning(false, NULL);
onClickStartStop();
}

L'événement onClickStartStop() permet de démarrer/arrêter le serveur.

Le serveur d'applications peut sérialiser les réponses à envoyer aux clients de plusieurs façons : ce paramètre est disponible avec la combobox cboSerializationType. Pour plus d'informations sur les différents types de sérialisation, rendez-vous sur la FAQ de la bibliothèque QxOrm. D'une manière générale, la sérialisation binaire est fortement conseillée pour une transaction réseau, car elle est plus rapide à exécuter et permet de limiter le trafic sur le réseau.

On définit également le port d'écoute du serveur d'applications avec le champ spinPortNumber.

Un paramètre important est le nombre de threads disponibles sur le serveur d'applications : cela correspond au nombre de clients pouvant se connecter au serveur simultanément. La valeur par défaut de ce paramètre est 30, vous pouvez modifier cette valeur suivant la charge estimée de votre serveur d'applications. Si le nombre de clients dépasse le nombre de threads disponibles, la requête est mise en attente : dès qu'un thread se libère, alors la requête s'exécute normalement. Tout ceci est géré automatiquement par la bibliothèque QxOrm : il est juste important de faire une estimation de la charge que pourra avoir votre serveur d'applications.

Enfin, l'appel à onClickStartStop() permet de démarrer automatiquement le serveur dès l'exécution du programme qxServer.

Pour aller plus loin : le module QxService de la bibliothèque QxOrm propose un mécanisme simple à base de threads pour mettre en place un serveur d'applications C++. Une solution basée uniquement sur la notion de threads peut présenter des inconvénients. Que se passe-t-il si le programme serveur plante ? Que se passe-t-il si le programme serveur est surchargé ? Il peut être envisagé de créer un manager de serveurs, situé entre les clients et les serveurs, afin de rediriger de manière optimale les requêtes des clients (un client est mis en relation avec un serveur par le manager des serveurs). Par exemple, le système de virtualisation Citrix (logiciel serveur permettant de distribuer des applications ou des services sur un réseau et d'y accéder à distance à partir de clients légers) est basé sur ce type d'architecture.

II-C - Description du fichier main_dlg.cpp, méthode loadServices()

fichier main_dlg.cpp, méthode loadServices()

void main_dlg::loadServices()
{
// Nécessaire pour être certain de charger les DLL contenant les services : création d'un service fantôme pour chaque DLL
// Il peut être intéressant à ce niveau de créer un mécanisme de plug-ins pour charger les différents services
server_infos dummy_01; Q_UNUSED(dummy_01);
}

La méthode loadServices() est l'unique dépendance avec les services proposés par le serveur d'applications. Elle sert uniquement à créer une instance fantôme pour être certain que la DLL contenant la liste des services soit correctement chargée au démarrage de l'application. Pour un logiciel en production, il peut être intéressant à ce niveau de proposer un système de plug-ins pour charger les différents services.

II-D - Description du fichier main_dlg.cpp, méthode onClickStartStop()

fichier main_dlg.cpp, méthode onClickStartStop()

void main_dlg::onClickStartStop()
{
if (m_pThreadPool)
{
m_pThreadPool->disconnect();
m_pThreadPool.reset();
txtError->setPlainText("");
txtTransaction->setPlainText("");
onServerIsRunning(false, NULL);
}
else
{
qx::service::QxConnect::getSingleton()->setPort(spinPortNumber->value());
qx::service::QxConnect::getSingleton()->setThreadCount(spinThreadCount->value());
qx::service::QxConnect::getSingleton()->setSerializationType((qx::service::QxConnect::serialization_type)
(cboSerializationType->itemData(cboSerializationType->currentIndex()).toInt()));
qx::service::QxConnect::getSingleton()->setCompressData(chkCompressData->isChecked());
qx::service::QxConnect::getSingleton()->setEncryptData(chkEncryptData->isChecked());

m_pThreadPool.reset(new qx::service::QxThreadPool());
QObject::connect(m_pThreadPool.get(), SIGNAL(error(const QString &, qx::service::QxTransaction_ptr)), this,
SLOT(onError(const QString &, qx::service::QxTransaction_ptr)));
QObject::connect(m_pThreadPool.get(), SIGNAL(serverIsRunning(bool, qx::service::QxServer *)), this,
SLOT(onServerIsRunning(bool, qx::service::QxServer *)));
QObject::connect(m_pThreadPool.get(), SIGNAL(transactionFinished(qx::service::QxTransaction_ptr)), this,
SLOT(onTransactionFinished(qx::service::QxTransaction_ptr)));
m_pThreadPool->start();
}
}

La méthode onClickStartStop() permet de démarrer/arrêter le serveur d'applications : elle s'occupe de créer une instance de type qx::service::QxThreadPool_ptr ou bien de la détruire. Si la variable m_pThreadPool est valorisée, alors cela signifie que l'on souhaite arrêter le serveur : m_pThreadPool.reset();. Sinon, le serveur est arrêté, on souhaite donc le démarrer :

m_pThreadPool.reset(new qx::service::QxThreadPool());
m_pThreadPool->start();

Le paramétrage du serveur est effectué grâce au singleton qx::service::QxConnect::getSingleton(). Enfin, l'interface utilisateur s'abonne aux événements envoyés par le serveur d'applications (mécanisme de signaux et de slots de Qt) pour récupérer une erreur ou bien afficher la dernière transaction client-serveur.

II-E - Description du fichier main_dlg.cpp, méthodes onError() et onTransactionFinished()

fichier main_dlg.cpp, méthodes onError() et onTransactionFinished()

void main_dlg::onError(const QString & err, qx::service::QxTransaction_ptr transaction)
{
if (err.isEmpty()) { txtError->setPlainText(""); return; }
QString errText = QDateTime::currentDateTime().toString("dd.MM.yyyy hh:mm") + " : " + err;
if (transaction) { errText += QString("\r\n\r\n") + qx::serialization::xml::to_string(* transaction); }
txtError->setPlainText(errText.replace("\t", "    "));
}

void main_dlg::onTransactionFinished(qx::service::QxTransaction_ptr transaction)
{
if (! transaction) { txtTransaction->setPlainText(""); return; }
QString text = qx::serialization::xml::to_string(* transaction);
txtTransaction->setPlainText(text.replace("\t", "    "));
}

Toutes les transactions entre client et serveur sont représentées par la classe qx::service::QxTransaction_ptr. Cette classe contient toutes les informations nécessaires à l'exécution d'un service (identifiant unique, date-heure, requête du client, service à exécuter, réponse du serveur, code et message d'erreur, etc.). La transaction est sérialisée au format XML avant d'être affichée à l'utilisateur dans le champ txtTransaction. Cette sérialisation est indépendante de la réponse envoyée au client qui, par défaut, est au format binaire.

II-F - Résultat obtenu pour le projet qxServer

Et... c'est tout : vous pouvez constater que l'écriture d'un serveur d'applications est extrêmement simple avec la bibliothèque QxOrm. Votre serveur d'applications est prêt pour proposer de multiples services aux différents clients. Voici le résultat obtenu :

III - Création de la couche service : qxService

La couche service doit être partagée entre le client et le serveur. La compilation du projet qxService crée deux DLL (ou fichiers SO sous Linux) : qxServiceClient et qxServiceServer. Une option de compilation _QX_SERVICE_MODE_CLIENT permet de distinguer client et serveur. L'outil qmake de Qt et le système de fichiers PRO et PRI permettent de créer facilement ce type d'architecture :

- le fichier qxService.pri correspond au tronc commun des deux DLL, c'est-à-dire l'ensemble des dépendances et des fichiers à compiler ;

- le fichier qxServiceClient.pro est spécifique au mode client : définition de l'option de compilation _QX_SERVICE_MODE_CLIENT et du nom de la DLL ;

- le fichier qxServiceServer.pro est spécifique au mode serveur : définition du nom de la DLL.

Il est important de signaler que ce mécanisme permet au programme client de partager les mêmes fichiers que le programme serveur. La partie cliente n'a aucun code à écrire pour appeler un service : le serveur peut livrer la liste des fichiers d'en-tête ainsi que les bibliothèques partagées (par exemple *.dll et *.lib avec Visual C++).

III-A - Écriture du premier service : récupérer la date et l'heure courantes du serveur

Le premier service proposé par le serveur d'applications de test est relativement simple : il consiste à renvoyer aux clients la date et l'heure courantes du serveur. Ce service est disponible avec la classe server_infos : fichiers server_infos.h et server_infos.cpp. Une même classe peut proposer plusieurs services : la classe server_infos pourrait par exemple renvoyer en plus de la date et l'heure courantes, un nom de machine, une fréquence processeur du serveur, etc.

Chaque classe service possède des paramètres d'entrée (demande du client) et des paramètres de sortie (réponse du serveur). Une classe paramètre (entrée ou sortie) doit hériter de la classe qx::service::IxParameter et doit être sérialisable. Une classe service doit hériter de la classe template qx::service::QxService<INPUT, OUTPUT> et doit définir une liste de méthodes (services disponibles). Il est conseillé d'écrire les classes, paramètres d'entrée, paramètres de sortie et services, dans le même fichier.

III-B - Description du fichier server_infos.h

fichier server_infos.h

#ifndef _QX_SERVICE_SERVER_INFOS_H_
#define _QX_SERVICE_SERVER_INFOS_H_

/* -- Liste des paramètres d'entrée du service -- */

class QX_SERVICE_DLL_EXPORT server_infos_input : public qx::service::IxParameter
{ ; };

QX_REGISTER_HPP_QX_SERVICE(server_infos_input, qx::service::IxParameter, 0)
typedef boost::shared_ptr<server_infos_input> server_infos_input_ptr;

/* -- Liste des paramètres de sortie du service -- */

class QX_SERVICE_DLL_EXPORT server_infos_output : public qx::service::IxParameter
{ public: QDateTime current_date_time; };

QX_REGISTER_HPP_QX_SERVICE(server_infos_output, qx::service::IxParameter, 0)
typedef boost::shared_ptr<server_infos_output> server_infos_output_ptr;

/* -- Définition du service -- */

typedef qx::service::QxService<server_infos_input, server_infos_output> server_infos_base_class;
class QX_SERVICE_DLL_EXPORT server_infos : public server_infos_base_class
{
public:
server_infos() : server_infos_base_class("server_infos") { ; }
virtual ~server_infos() { ; }
void get_current_date_time();
};

QX_REGISTER_HPP_QX_SERVICE(server_infos, qx::service::IxService, 0)
typedef boost::shared_ptr<server_infos> server_infos_ptr;

#endif // _QX_SERVICE_SERVER_INFOS_H_

Le fichier server_infos.h possède trois classes :

- server_infos_input : hérite de qx::service::IxParameter et correspond aux paramètres d'entrée du service (demande du client). Le service de test n'a pas besoin de paramètres en entrée, cette classe ne contient donc aucune propriété ;

- server_infos_output : hérite de qx::service::IxParameter et correspond aux paramètres de sortie du service (réponse du serveur). Cette classe contient une seule propriété, la date et l'heure courantes du serveur (QDateTime current_date_time) ;

- server_infos : hérite de qx::service::QxService<INPUT, OUTPUT> et contient la liste des services disponibles : une seule méthode pour récupérer la date et l'heure courantes du serveur.

Ces trois classes doivent être enregistrées dans le contexte QxOrm, de la même façon qu'une classe persistante (voir le tutoriel qxBlog). C'est pourquoi on utilise la macro QX_REGISTER_HPP_QX_SERVICE pour ces trois classes. De plus, pour simplifier l'écriture des pointeurs, la gestion de la mémoire et éviter les problèmes de fuites mémoires, on utilise les pointeurs intelligents de la bibliothèque boost : boost::shared_ptr. Le module QxService travaille essentiellement avec des pointeurs intelligents, c'est pourquoi il est fortement conseillé de créer les typedef correspondants, par exemple :

typedef boost::shared_ptr<server_infos_input> server_infos_input_ptr;
typedef boost::shared_ptr<server_infos_output> server_infos_output_ptr;
typedef boost::shared_ptr<server_infos> server_infos_ptr;

Enfin, le constructeur du service doit indiquer en paramètre le nom de la classe sous forme de chaîne de caractères : ceci est indispensable pour le moteur d'introspection de QxOrm pour pouvoir instancier dynamiquement les services correspondant aux requêtes des clients.

III-C - Description du fichier server_infos.cpp

fichier server_infos.cpp

#include "../../include/precompiled.h"

#include "../../include/service/server_infos.h"

#include <QxMemLeak.h>

QX_REGISTER_CPP_QX_SERVICE(server_infos_input)
QX_REGISTER_CPP_QX_SERVICE(server_infos_output)
QX_REGISTER_CPP_QX_SERVICE(server_infos)

namespace qx {

template <> void register_class(QxClass<server_infos_input> & t)
{ Q_UNUSED(t); }

template <> void register_class(QxClass<server_infos_output> & t)
{ t.data(& server_infos_output::current_date_time, "current_date_time"); }

template <> void register_class(QxClass<server_infos> & t)
{ t.fct_0<void>(& server_infos::get_current_date_time, "get_current_date_time"); }

} // namespace qx

#ifdef _QX_SERVICE_MODE_CLIENT

void server_infos::get_current_date_time()
{ qx::service::execute_client(this, "get_current_date_time"); }

#else // _QX_SERVICE_MODE_CLIENT

void server_infos::get_current_date_time()
{
server_infos_output_ptr output = server_infos_output_ptr(new server_infos_output());
output->current_date_time = QDateTime::currentDateTime();
setOutputParameter(output);
setMessageReturn(true);
}

#endif // _QX_SERVICE_MODE_CLIENT

Le fichier server_infos.cpp contient l'implémentation du service pour le mode client et le mode serveur : c'est la macro _QX_SERVICE_MODE_CLIENT qui fait la distinction entre client et serveur au moment de la compilation du projet. La macro QX_REGISTER_CPP_QX_SERVICE permet d'enregistrer les trois classes dans le contexte QxOrm, de la même façon qu'une classe persistante (voir le tutoriel qxBlog). Ensuite, on écrit la méthode de mapping void qx::register_class(...) pour les trois classes du service :

- les deux classes de paramètres enregistrent les propriétés utilisées pour effectuer une demande du client (aucune pour le service de test), et les propriétés qui seront renvoyées pour la réponse du serveur (date et heure courantes : t.data(& server_infos_output::current_date_time, "current_date_time");) ;

- la classe service doit enregistrer la liste des méthodes disponibles, ici : t.fct_0<void>(& server_infos::get_current_date_time, "get_current_date_time");.

Remarque : toutes les méthodes de type service doivent avoir la même signature : retour de type void et pas d'argument (par exemple : void my_service()). En effet, dans un service, les paramètres d'entrée sont disponibles par la méthode getInputParameter() (de type server_infos_input_ptr dans cet exemple). Les paramètres de sortie peuvent être valorisés par la méthode setOutputParameter() (de type server_infos_output_ptr dans l'exemple). Une valeur de retour de type qx_bool permet d'indiquer que la transaction s'est déroulée normalement, ou bien qu'une erreur quelconque est survenue (avec libellé et code de l'erreur). Il est très important d'écrire setMessageReturn(true); à la fin de chaque méthode service pour indiquer que tout s'est bien déroulé.

La dernière partie du fichier contient l'implémentation de la méthode server_infos::get_current_date_time() pour le mode client et serveur :

- pour le mode client, le code est très simple et sera le même pour tous les services : qx::service::execute_client(this, "get_current_date_time"); ;

- pour le mode serveur, le service de test est très simple : on valorise la date et l'heure courantes, on la transfère dans les paramètres de sortie, puis on indique que la transaction s'est déroulée sans aucune erreur.

III-D - Écriture du second service : opérations avec une classe persistante

Le projet qxService contient un second exemple de service plus complet avec une classe persistante (classe user), et des actions sur une base de données (SELECT, INSERT, UPDATE, DELETE, etc.). Ce deuxième exemple fait transiter sur le réseau des structures complexes : pointeurs, pointeurs intelligents, collections, critères de recherche, etc. On ne détaillera pas ce second service dans le tutoriel, le principe étant identique au premier service :

services pour gérer une classe persistante

#include "../../include/precompiled.h"

#include "../../include/service/user_service.h"

#include "../../include/dao/user_manager.h"

#include <QxMemLeak.h>

QX_REGISTER_CPP_QX_SERVICE(user_service_input)
QX_REGISTER_CPP_QX_SERVICE(user_service_output)
QX_REGISTER_CPP_QX_SERVICE(user_service)

namespace qx {

template <> void register_class(QxClass<user_service_input> & t)
{
t.data(& user_service_input::id, "id");
t.data(& user_service_input::user, "user");
t.data(& user_service_input::criteria, "criteria");
}

template <> void register_class(QxClass<user_service_output> & t)
{
t.data(& user_service_output::user, "user");
t.data(& user_service_output::list_of_users, "list_of_users");
}

template <> void register_class(QxClass<user_service> & t)
{
t.fct_0<void>(& user_service::insert, "insert");
t.fct_0<void>(& user_service::update, "update");
t.fct_0<void>(& user_service::remove, "remove");
t.fct_0<void>(& user_service::remove_all, "remove_all");
t.fct_0<void>(& user_service::fetch_by_id, "fetch_by_id");
t.fct_0<void>(& user_service::fetch_all, "fetch_all");
t.fct_0<void>(& user_service::get_by_criteria, "get_by_criteria");
}

} // namespace qx

#ifdef _QX_SERVICE_MODE_CLIENT

void user_service::insert()            { qx::service::execute_client(this, "insert"); }
void user_service::update()            { qx::service::execute_client(this, "update"); }
void user_service::remove()            { qx::service::execute_client(this, "remove"); }
void user_service::remove_all()        { qx::service::execute_client(this, "remove_all"); }
void user_service::fetch_by_id()       { qx::service::execute_client(this, "fetch_by_id"); }
void user_service::fetch_all()         { qx::service::execute_client(this, "fetch_all"); }
void user_service::get_by_criteria()   { qx::service::execute_client(this, "get_by_criteria"); }

#else // _QX_SERVICE_MODE_CLIENT

void user_service::insert()
{
user_service_input_ptr input = getInputParameter();
if (! input) { setMessageReturn(0, "invalid input parameter to call service 'user_service::insert()'"); return; }
QSqlError err = user_manager().insert(input->user);
if (err.isValid()) { setMessageReturn(0, err.text()); return; }
user_service_output_ptr output = user_service_output_ptr(new user_service_output());
output->user = input->user;
setOutputParameter(output);
setMessageReturn(true);
}

void user_service::update()
{
user_service_input_ptr input = getInputParameter();
if (! input) { setMessageReturn(0, "invalid input parameter to call service 'user_service::update()'"); return; }
QSqlError err = user_manager().update(input->user);
if (err.isValid()) { setMessageReturn(0, err.text()); }
else { setMessageReturn(true); }
}

void user_service::remove()
{
user_service_input_ptr input = getInputParameter();
if (! input) { setMessageReturn(0, "invalid input parameter to call service 'user_service::remove()'"); return; }
user_ptr user_tmp = user_ptr(new user());
user_tmp->id = input->id;
QSqlError err = user_manager().remove(user_tmp);
if (err.isValid()) { setMessageReturn(0, err.text()); }
else { setMessageReturn(true); }
}

void user_service::remove_all()
{
QSqlError err = user_manager().remove_all();
if (err.isValid()) { setMessageReturn(0, err.text()); }
else { setMessageReturn(true); }
}

void user_service::fetch_by_id()
{
user_service_input_ptr input = getInputParameter();
if (! input) { setMessageReturn(0, "invalid input parameter to call service 'user_service::fetch_by_id()'"); return; }
user_ptr user_output = user_ptr(new user());
user_output->id = input->id;
QSqlError err = user_manager().fetch_by_id(user_output);
if (err.isValid()) { setMessageReturn(0, err.text()); return; }
user_service_output_ptr output = user_service_output_ptr(new user_service_output());
output->user = user_output;
setOutputParameter(output);
setMessageReturn(true);
}

void user_service::fetch_all()
{
list_of_users_ptr list_of_users_output = list_of_users_ptr(new list_of_users());
QSqlError err = user_manager().fetch_all(list_of_users_output);
if (err.isValid()) { setMessageReturn(0, err.text()); return; }
user_service_output_ptr output = user_service_output_ptr(new user_service_output());
output->list_of_users = list_of_users_output;
setOutputParameter(output);
setMessageReturn(true);
}

void user_service::get_by_criteria()
{
user_service_input_ptr input = getInputParameter();
if (! input) { setMessageReturn(0, "invalid input parameter to call service 'user_service::get_by_criteria()'"); return; }
list_of_users_ptr list_of_users_output = list_of_users_ptr(new list_of_users());
QSqlError err = user_manager().get_by_criteria(input->criteria, list_of_users_output);
if (err.isValid()) { setMessageReturn(0, err.text()); return; }
user_service_output_ptr output = user_service_output_ptr(new user_service_output());
output->list_of_users = list_of_users_output;
setOutputParameter(output);
setMessageReturn(true);
}

#endif // _QX_SERVICE_MODE_CLIENT

À ce niveau du tutoriel, le serveur d'applications C++ est terminé et propose plusieurs services. Il reste à présent à écrire le code client qui va appeler tous les services mis en place...

IV - Création de l'interface cliente : qxClient

De la même façon que le projet qxServer, le projet qxClient possède une interface utilisateur construite avec l'outil Qt Designer de la bibliothèque Qt. Cette interface possède plusieurs boutons pour appeler l'ensemble des services proposés par le serveur d'applications. L'interface permet également d'indiquer une adresse IP et un numéro de port pour se connecter au serveur d'applications.

IV-A - Description de la méthode onClickBtnDateTime()

Comment récupérer la date et l'heure courantes du serveur d'applications ? Voici le code qui s'exécute lorsque l'utilisateur clique sur le bouton Get Server DateTime :

méthode onClickBtnDateTime()

void main_dlg::onClickBtnDateTime()
{
// Création d'une instance de service et appel à la méthode pour recevoir la date et l'heure courantes du serveur
server_infos service;
service.get_current_date_time();
// Affiche la dernière transaction au format XML
updateLastTransactionLog(service.getTransaction());
}

Comme on peut le constater, la partie cliente n'a aucun code spécifique pour appeler un service. Il suffit d'instancier un service, puis d'appeler la méthode intéressante : get_current_date_time(). La méthode updateLastTransactionLog() permet d'afficher la dernière transaction client-serveur (au format XML) exécutée. Si une erreur s'est produite, alors un message apparaît à l'écran pour le signaler à l'utilisateur. Pour savoir si le service s'est exécuté correctement, il faut utiliser la méthode : service.getMessageReturn(); (de type qx_bool qui peut contenir un code et un libellé d'une éventuelle erreur). Enfin, pour récupérer la réponse du serveur (donc la date et l'heure courantes), il faut utiliser la méthode : service.getOutputParameter(); (de type user_service_output_ptr).

IV-B - Description de la méthode onClickBtnDateTimeAsync()

méthode onClickBtnDateTimeAsync()

void main_dlg::onClickBtnDateTimeAsync()
{
if (m_pDateTimeAsync) { qDebug("[QxOrm] '%s' transaction is already running", "server_infos::get_current_date_time"); return; }
// Création d'une instance de service et appel de la méthode pour recevoir la date et l'heure courantes du serveur (mode asynchrone)
server_infos_ptr service = server_infos_ptr(new server_infos());
m_pDateTimeAsync.reset(new qx::service::QxClientAsync());
QObject::connect(m_pDateTimeAsync.get(), SIGNAL(finished()), this, SLOT(onDateTimeAsyncFinished()));
m_pDateTimeAsync->setService(service, "get_current_date_time");
m_pDateTimeAsync->start();
}

void main_dlg::onDateTimeAsyncFinished()
{
if (! m_pDateTimeAsync || ! m_pDateTimeAsync->getService()) { return; }
updateLastTransactionLog(m_pDateTimeAsync->getService()->getTransaction());
m_pDateTimeAsync.reset();
}

Ce second exemple correspond au bouton Get Server DateTime Async de l'interface utilisateur. Il montre comment appeler un service de manière asynchrone, c'est-à-dire sans bloquer l'IHM en attendant la réponse du serveur. La bibliothèque QxOrm propose la classe qx::service::QxClientAsync pour simplifier les appels asynchrones.

Le mécanisme des appels asynchrones avec le module QxService est très simple :

- création d'une instance d'un service ;

- création d'une instance de type qx::service::QxClientAsync ;

- connexion à l'événement finished (pour indiquer qu'une réponse du serveur vient d'arriver) ;

- passage de l'instance du service et de la méthode à appeler (sous forme de chaîne de caractères) à l'objet qx::service::QxClientAsync ;

- démarrage de la transaction avec l'appel de la méthode start().

IV-C - Description de la méthode onClickBtnAddUser()

méthode onClickBtnAddUser()

void main_dlg::onClickBtnAddUser()
{
// Création des paramètres d'entrée contenant l'utilisateur à ajouter en base de données
user_service_input_ptr input = user_service_input_ptr(new user_service_input());
input->user = fileUser();
// Création d'une instance de service et association des paramètres d'entrée
user_service service;
service.setInputParameter(input);
service.insert();
// Si la transaction s'est déroulée correctement, on affiche l'identifiant qui vient d'être ajouté en base
user_ptr output = (service.isValidWithOutput() ? service.getOutputParameter()->user : user_ptr());
if (output) { fillUser(output); }
// Affiche la dernière transaction au format XML
updateLastTransactionLog(service.getTransaction());
}

Ce troisième exemple correspond au bouton Add dans la section User transaction. Il permet à l'utilisateur d'ajouter une nouvelle personne dans la base de données. Cet exemple montre comment passer une structure (classe user) en paramètre d'entrée d'un service. La méthode fileUser() permet de créer une instance de type user et de valoriser ses propriétés en fonction des champs de l'IHM. Cette instance est ensuite utilisée comme paramètre d'entrée du service. Si la transaction s'est déroulée correctement, le paramètre de retour (réponse du serveur) contient lui aussi une instance de type user avec le nouvel identifiant qui vient d'être ajouté en base de données. On utilise alors la méthode fillUser() pour mettre à jour l'interface utilisateur en fonction de la réponse du serveur et afficher ainsi le nouvel identifiant.

IV-D - Description de la méthode onClickBtnGetAllUsers()

méthode onClickBtnGetAllUsers()

void main_dlg::onClickBtnGetAllUsers()
{
// Création d'une instance de service
user_service service;
service.fetch_all();
// Si la transaction s'est déroulée correctement, affiche un message avec le nombre d'utilisateurs stockés en base
list_of_users_ptr output = (service.isValidWithOutput() ? service.getOutputParameter()->list_of_users : list_of_users_ptr());
if (output) { QMessageBox::information(this, "qxClient - get all users", "database contains '" + QString::number(output->size()) + "' user(s)."); }
// Affiche la dernière transaction au format XML
updateLastTransactionLog(service.getTransaction());
}

Ce quatrième exemple correspond au bouton Get All de la section User transaction. Il permet de récupérer la liste de tous les user présents dans la base de données. Le paramètre de retour est une liste fortement typée : il est possible d'utiliser les collections des bibliothèques STL, boost, Qt ou qx::QxCollection. Le module QxService permet donc d'échanger des structures complexes entre client et serveur.

A présent, bon courage avec le module QxService... ;o)

V - Remerciements

Je remercie tout particulièrement Thibaut Cuvelier pour ses conseils pour l'amélioration de ce tutoriel.

Je remercie également _Max_ pour ses relectures et corrections orthographiques.