![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
QxOrm >> Tutoriel >> qxClientServer | Version courante : QxOrm 1.1.6 (LGPL) - documentation en ligne de la bibliothèque QxOrm | ![]() |
![]() |
![]() Remarque : pour plus de détails sur la notion de socket, de thread, de réseau, etc... le site de Qt propose des tutoriels sur l'utilisation du module QtNetwork :
1- 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 librarie Qt. Cette interface a pour seul objectif d'afficher à l'utilisateur la dernière transaction client-serveur, et de pouvoir configurer certains paramètres du serveur. Pour une utilisation réelle (logiciel de production), il est conseillé de proposer un sytè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 la plus optimale pour un serveur d'applications. Les fichiers main_dlg.h et main_dlg.cpp correspondent au code C++ de l'interface de qxServer :
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 librarie QxOrm. La méthode init() permet d'initialiser les paramètres par défaut du serveur, de connecter les évènements SIGNAL-SLOT 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...
L'évènement onClickStartStop() permet de démarrer/arrêter le serveur. Le serveur d'applications peut serializer 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 serialization, suivre ce lien de la FAQ. D'une manière générale, la serialization binaire est fortement conseillée pour une transaction réseau car elle est plus rapide à exécuter et permet de limiter le traffic 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 : celà correspond aux nombres 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 librarie 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.
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 soient 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 plugins pour charger les différents services.
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 celà signifie que l'on souhaite arrêter le serveur : m_pThreadPool.reset();. Sinon, le serveur est arrêté donc on souhaite 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 SIGNAL-SLOT de Qt) pour récupérer une erreur ou bien afficher la dernière transaction client-serveur.
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 serializée au format xml avant d'être affichée à l'utilisateur dans le champ txtTransaction. Cette serialization est indépendante de la réponse envoyée au client qui, par défaut, est au format binaire. Et... c'est tout : vous pouvez constater que l'écriture d'un serveur d'applications est extrêmement simple avec la librarie QxOrm. Votre serveur d'applications est prêt pour proposer de multiples services aux différents clients. Voici le résultat obtenu : ![]() 2- 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 2 dll (ou fichiers *.so sous Linux) : qxServiceClient et qxServiceServer. Une option de compilation _QX_SERVICE_MODE_CLIENT permet de faire la distinction entre le client et le 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 2 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 pouvoir appeler un service : le serveur peut livrer la liste des fichiers de type headers, les .dll et .lib (ou *.so sous Linux). Le 1er service proposé par notre serveur d'applications de test est relativement simple : il consiste à renvoyer aux clients la date-heure courante 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-heure courante, 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érializable. Une classe service doit hériter du 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.
Le fichier server_infos.h possède 3 classes : * server_infos_input : hérite de qx::service::IxParameter et correspond aux paramètres d'entrée du service (demande du client). Notre service de test n'a pas besoin de paramètres en entrée, donc cette classe ne contient 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-heure courante du serveur. * 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-heure courante du serveur. Ces 3 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 nous utilisons la macro QX_REGISTER_HPP_QX_SERVICE pour ces 3 classes. De plus, pour simplifier l'écriture des pointeurs, la gestion de la mémoire et éviter les problèmes de fuites mémoires, nous utilisons les pointeurs intelligents de la librairie 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;. 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.
Le fichier server_infos.cpp contient l'implémentation du service pour le mode client et le mode serveur. La macro QX_REGISTER_CPP_QX_SERVICE permet d'enregistrer les 3 classes dans le contexte QxOrm, de la même façon qu'une classe persistante (voir le tutoriel qxBlog). Ensuite, nous écrivons la méthode de mapping void qx::register_class(...) pour les 3 classes du service : * Les 2 classes de paramètres enregistrent les propriétés utilisées pour effectuer une demande du client (aucune pour notre service de test), et les propriétés qui seront renvoyées pour la réponse du serveur (date-heure courante : t.data(& server_infos_output::current_date_time, "current_date_time");). * La classe service doit enregistrer la liste des méthodes disponibles, dans notre cas : 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 : pas de valeur de retour, 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 notre exemple). Les paramètres de sortie peuvent être valorisés par la méthode setOutputParameter() (de type server_infos_output_ptr dans notre 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 de notre 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, notre service de test est très simple : on valorise la date-heure courante, on la transfère dans les paramètres de sortie, puis on indique que la transaction s'est déroulée sans aucune erreur. Remarque : le projet qxService contient un 2ème 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 2ème exemple fait transiter sur le réseau des structures complexes : pointeurs, pointeurs intelligents, collections, critères de recherche, etc... Nous ne détaillerons pas ce second service dans le tutoriel, le principe étant identique au 1er service :
A ce niveau du tutoriel, notre serveur d'applications C++ est terminé et propose plusieurs services. Il reste à présent à écrire le code client qui va appeler tous les services que nous avons mis en place... 3- 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 librarie Qt. Cette interface possède plusieurs boutons pour appeler l'ensemble des services proposés par notre serveur d'applications. L'interface permet également d'indiquer une adresse ip et un n° de port pour se connecter au serveur d'applications. ![]() Comment récupérer la date-heure courante du serveur d'applications ? Voici le code qui s'exécute lorsque l'utilisateur clique sur le bouton Get Server DateTime :
Comme vous pouvez le constater, la partie cliente n'a aucun code spécifique à écrire pour pouvoir appeler un service. Il suffit d'instancier un service, puis d'appeler la méthode qui nous intéresse : 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 sa date-heure courante), il faut utiliser la méthode : service.getOutputParameter(); (de type user_service_output_ptr).
Ce 2ème 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 librairie 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 chaine de caractères) à l'objet qx::service::QxClientAsync * démarrage de la transaction avec l'appel de la méthode start()
Ce 3è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 nous 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 de notre 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.
Ce 4è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 : 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) |
![]() |