Programmation Client Serveur

Ecriture d'un serveur

Préface

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.


Analyse théorie et algorithme

Dans cet article, on appellera 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 quand à 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....

Rappel sur les sockets

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.

Cahier des charges du serveur

Le serveur devra pouvoir gérer simultanément plusieurs connexions. Un protocole sera défini qui spécifiera les commandes comprises par le serveur, ainsi que l'ordre dans lequel les commandes devront être passées.

Fonctionnement du service

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 d'adresse email. 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 prenom Le serveur recherche le contact nom prenom et s'il le trouve renvoie +OK adresseEmail sinon -ERR
  • QUIT le serveur met fin à la connection

Formalisons la connexion
				[LOGIN [PASS [ADR*]]]QUIT
			
			légende:
							[]=facultatif				
							*= répétable
			

Programmation du serveur

Toute la programmation va consister à implémenter notre protocole dans la fonction traiteConnexion. Nous allons donc élaborer notre algorithme qui implémente le protocole établi 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)
	// à 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 puisqu'on est sûr qu'à l'adresse en question
	// il y a bien une instance de la classe socketClient
	
	socketClient * canal = (socketClient *)sc;
	
	//on utilisera tois méthodes de la classe socketClient:
	// string socketClient::lit(int nbMaxCar)
	// 			recois 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 connection

	
	/************** 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
					//si on le trouve
						//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
						//verification 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 emails ***
				//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'email trouvée
							//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 qu'on va faire avant de le faire. L'algorithme dans le source rend la compréhension du source plus facile.

Mise en oeuvre

Vous allez avoir besoin de deux fichiers sources: Télécharger 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: On peut 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 on ajoutera donc -D REENTRANT. g++ -D REENTRANT serveur.cpp -o monSuperServeur -lpthread

Exécution:
Pour lancer l'exécutable on fait 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