Résumé
La programmation client-serveur consiste à développer une application de façon modulaire, les différents modules obtenus communiquent entre eux via le réseau et peuvent donc ainsi être lancés sur des machines différentes. Le but de cet article est de démystifier la programmation de serveurs.
Table des matières
Nous allons voir comment programmer une connexion client/serveur.
Dans cet article, nous appellerons serveur une application fournissant un service accessible via le réseau à d'autres applications appelées les clients. Le rôle d'un serveur est de fournir un service. On trouve des serveurs de temps qui fournissent l'heure, des serveurs de météo qui fournissent des renseignements quant à la température qu'il fait en un lieu, des serveurs de news, des serveurs pop, imap, ftp, www, ssh. Mais on peut bien sûr aussi inventer son propre serveur, un serveur de prix, un serveur d'annuaire, un serveur de toute information imaginable, disponibilité d'un article, programme télé etc....
Pour accéder à un service d'un serveur, le client ouvre une socket vers l'ip du serveur et le port associé au service. L'article sur le développement des clients : ici explique en détail ce qu'est une socket ainsi qu'un protocole.
Le serveur devra pouvoir gérer simultanément plusieurs connexions. Un protocole sera défini et spécifiera les commandes comprises par le serveur, ainsi que l'ordre dans lequel les commandes devront être passées.
Le serveur est un programme. Il n'est pas activé à la demande . Il tourne sans arrêt dans une boucle infinie (while (true)). Dans cette boucle, il attend la connexion éventuelle d'un client. Chaque connexion d'un client donnera lieu à la création d'un nouveau processus (Thread). Ce processus exécutera la fonction traiteConnexion. Cette fonction sera constituée d'une boucle contenant l'attente de la saisie de la commande par le client, suivie du traitement de cette commande. La sortie de boucle sera déclenchée par une commande spécifique envoyée par le client.
Supposons que l'on veuille faire un serveur de courriels. Il nous faut définir notre protocole.
Liste des commandes et réaction du serveur:
LOGIN <nom> Le serveur vérifie le login et renvoie +OK ou -ERR
PASS <pass> Le serveur vérifie le pass et renvoie +OK ou -ERR
ADR nom prénom Le serveur recherche le contact nom prénom et s'il le trouve renvoie +OK adresseEmail sinon -ERR
QUIT le serveur met fin à la connexion
Formalisons la connexion
[LOGIN [PASS [ADR*]]]QUIT
légende: []=facultatif *= répétable
Toute la programmation va consister à implémenter notre protocole dans la fonction traiteConnexion. Nous allons donc élaborer notre algorithme qui implémente le protocole établit ci-dessus. Les crochets correspondent à des structures alternatives, et les * à des boucles. Le protocole est relativement simple et voici donc un premier jet de notre algorithme.
void * traiteConnexion(void * sc)
{
//Notre fonction reçoit une adresse en paramétre (sc pointeur sur variable non
//typée)
// A cette adresse nous trouverons l'instance de la classe socketClient créée
// lors de la connexion du client
// On va caster l'adresse reçue puisque nous sommes sûrs qu'à l'adresse en question
// il y a bien une instance de la classe socketClient
socketClient * canal = (socketClient *)sc;
//Nous utiliserons trois méthodes de la classe socketClient:
// string socketClient::lit(int nbMaxCar)
// reçoit une ligne du client et la renvoie sous forme de chaîne
// void socketClient::ecrit(string message)
// envoie une chaîne de caractères au client
// void socketClient::termine() met fin à la connexion
/************** début *************/
//envoi du message d'accueil
//mise à faux du drapeau LOGGUE
//***début de la boucle d'attente du login***
//lecture de la commande
//traitement de la commande
//si commande != QUIT
//si commande==LOGIN
//mise à faux du drapeau TROUVE
//recherche de l'utilisateur
//s'il est trouvé
//mise à vrai des drapeaux TROUVE et LOGGUE
//sinon
envoie de -ERR bad login
//fsi
//sinon
envoie de -ERR bad commande
//fsi
//fsi
//***fin de boucle sortie quand commande==QUIT ou LOGGUE***
//si LOGGUE alors
//***début de la boucle d'attente du pass***
//lecture de la commande
//traitement de la commande
//si commande != QUIT
//si commande==PASS
//mise à faux du drapeau IDENTIFIE
//vérification du mot de passe de l'utilisateur
//si identique
//mise à vrai du drapeau IDENTIFIE
//sinon
envoie de -ERR bad password
//fsi
//sinon
envoie de -ERR bad commande
//fsi
//fsi
//***fin de boucle sortie quand commande==QUIT ou IDENTIFIE***
//si IDENTIFIE alors
//***début de la boucle de recherche des adresses de courriels ***
//lecture de la commande
//traitement de la commande
//si commande != QUIT
//si commande==ADR
//mise à faux du drapeau adresseTrouvee
//recherche de l'adresse de l'utilisateur demandé
//si trouvé
//envoie au client de l'adresses de courriel trouvé
//sinon
envoie de -ERR contact inconnu
//fsi
//sinon
envoie de -ERR bad commande
//fsi
//fsi
//***fin de boucle sortie quand commande==QUIT***
//fsi IDENTIFIE
//fsi LOGGUE
//On met fin à la connexion
/************** fin *************/
}
L'algorithme est terminé ne reste que le codage. L'algorithmique est une démarche cartésienne qui consiste à décomposer un problème de façon à le simplifier. La démarche a abouti lorsqu'à l'ensemble des problèmes obtenus correspondent des solutions élémentaires aussi appelées primitives. Il faut savoir ce qui va être fait avant de le faire. L'algorithme dans le source rend la compréhension du source plus facile.
Vous allez avoir besoin de deux fichiers sources: Téléchargez l'archive.
La décompression de l'archive crée votre répertoire de travail.
Vous allez coder la fonction "traiteConnexion" du fichier serveur.cpp. Pour vous aider, inspirez-vous du petit listing ci-dessous.
void * traiteConnexion(void * sc)
{
socketClient * canal= (socketClient *) sc;
canal->ecrit("+OK Login:\r\n");
string login=canal->lit(255);
if(login=="BTSIG\r\n")
{
canal->ecrit("+OK mot de passe s'il te plait:\r\n");
string mdp=canal->lit(255);
if(mdp=="pass123\r\n")
{
canal->ecrit("+OK tu viens de gagner un 20/20 ciao:\r\n");
}
else
{
canal->ecrit("-ERR mauvais mot de passe\r\n");
}
}
else
{
canal->ecrit("-ERR mauvais login\r\n");
}
canal->ecrit("Au revoir \r\n");
canal->termine();
}
A noter: Quand le serveur reçoit une ligne elle peut être constituée de deux parties séparées par un espace. Exemple:
LOGIN Duval\r\n
Cette chaîne doit être analysée: Nous pouvons prendre les 5 premiers caractères pour obtenir la commande.
string commande=string(ligne,0,5);
Puis on prend les autres caractères du 7ième à la fin pour obtenir le nom d'utilisateur:
string nom=string(ligne,6,ligne.length(-(6+2)));
Pour compiler notre serveur, il faut le lier avec la bibliothéque
libpthread.so on pensera donc à ajouter -lpthread.
Notre code est réentrant, nous ajouterons donc -D REENTRANT.
g++ -D REENTRANT serveur.cpp -o monSuperServeur -lpthread
Exécution Pour lancer l'exécutable nous allons faire comme d'habitude ./monSuperServeur & dans un terminal. et pour le tuer un "ctrl c" fera l'affaire.
Pour le tester:
Dans un autre terminal
On lance le client universel telnet en lui passant en argument l'identifiant
du serveur ainsi que le numéro du port qui lui est associé.
Il suffit ensuite de se souvenir de son protocole,
envoyer des requêtes et recevoir des réponses.
telnet ipDuServeur portDuServeur