Ecrire des application réseau en Perl

Perl est un langage polyvalant. Il est cependant certains domaines dans lesquel il constituera la meilleur solution de par sa souplesse et sa simplicité d'utilisation. L'un de ces domaine est le réseaux. En effet, bon nombre de clients permettant d'automatiser des tâches réseau sont écrit en Perl.

Nous allons voir dans cet articles au combien il est simple d'écrire un serveur et un client dans un but expérimental. J'utilise le terme expérimental, car ne traiterons pas des aspects sécuritaire de l'implémentation réseau. Il sera a votre charge de s'assurer que vos script écrit dans un cadre de production sont fiable et sécurisé.

Avec un système de type Unix, on entend par le terme socket une extrémité d'un canal de communication. Il existe un grand nombre de sockets différent. Parmis eux, un type spécifique est fiable, ordonnée et simple a mettre en oeuvre : les streams. Un stream (ou flux en français) est un canal bidirectionnel qui peut être vu comme un tube (pipe).

Quelque soit le type de script réseau que vous comptez écrire, vous ferez sans doute appel au module IO::Socket intégré aux distributions de Perl. Ce module vous permettra de créer un nouveau socket. C'est lors de cette création que vous définirez vos choix et créerez le type de socket dont vous aurez besoin.

1. Un serveur TCP

Le protocole TCP vous permettra de créer des sessions entre le client et votre serveur. C'est habituellement le protocole le plus simple à mettre en oeuvre car tout la gestion de connexion y est déjà intégré.
Le but de notre premier script exemple est d'attendre patiamment qu'un client se connect pour que nous puissions lui répondre. Avant toute chose, nous devons charger les modules adéquates :
use strict;
use IO::Socket;
Vous reconnaîtrez ici le module indispensable à tout programmeur Perl qui se laisse insulter par le langage (tout en lui évitant de grosses erreurs) puis, ensuite, le module permettant l'accès aux fonctionnalité réseaux.
La première étape constituera à créer notre socket en utilisant la méthode new de l'objet IO::Socket::INET. Cette méthode nous retournera un objet de connexion qui représentera la serveur et que nous utiliserons ensuite à loisir.
La méthode new prend plusieurs option en argument. Ceci nous permettra de définir le type de socket que nous désirons créer :
my $serveur = IO::Socket::INET->new(
				LocalPort => 6018,
				Type => SOCK_STREAM,
				Listen => 1)
	or die "Impossible de devenir serveur : $@\n";
Les différente option que nous choisissons ici sont : En fin, si la création de socket est un échec, nous arrêtons immédiatement le script et affichons l'erreur. Il peut arriver en effet, que l'utilisateur qui lance le script ne possède pas les permissions nécessaure à l'ouverture de socket sur certain port. Les ports inférieurs à 1024 sont, par exemple réservés à l'utilisateur root (ou équivalent).

Nous pouvons ensuite nous pencher que la boucle d'attente des connexions. Cette boucle dépendra du résulatat de la méthode accept() que notre objet $serveur. Dès qu'une connexion se présentera sur le port que nous écoutons avec notre socket, $client représentera l'autre extrêminté de la connexion en cours : notre client.

while (my $client = $serveur->accept()) {
	print ">>> Debut de connexion\n";
	print $client "Salut le client !\n";
	while(my $ligne = <$client>) {
		print $ligne;
		if ($ligne =~ /^QUIT/i ) { close($client); }
		}
	print ">>> Fin de connexion\n";
	}
Une fois dans la boucle, nous ne nous proverons pas de signaler l'arrivé d'un client sur l'écran du serveur avec un simple print sur STDOUT. Nous en profitons pour saluer notre client en lui envoyant un message de bienvenue toujours à l'aide de print, mais cette fois directement sur l'objet $client.

Si nous arrêtons là le code de la boucle, notre client sera automatiquement déconnecté puisque notre travail s'arrête là. Mais comme notre mini-serveur doit bien servir à quelque chose, nous commençons une nouvelle boucle destinée à recevoir les informations en provenance de notre client. Notre socket étant de type stream, nous n'avons pas trop de soucis à nous faire et nous lisons sur l'objet $client comme s'il s'agissait d'un descripteur de fichier.

Nous recevons alors les informations ligne par ligne et les affichons sur le STDOUT du serveur. Enfin, sir le client nous envoi une ligne débutant par QUIT (quelquesoit la casse utilisé), nous coupons la connexion avec close($client); La connexion fermée, nous signalons ce fait sur le serveur.

Enfin, nous n'oublions pas de fermer la connexion en cas de problème. Cette ligne, c'est en principe jamais exécutée, mais restons prudent et codons propre.

close($serveur);
Nous somme maintenant fin pret à tester notre serveur. Un simple telnel fera l'affaire pour cela. Sur une première console, lancez le serveur et sur une seconde utilisez telnet sur le port adéquate :
$ telnet 127.0.0.1 6018
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Salut le client !
Le serveur nous réponde correctement et nous affiche le message de bienvenue. Coté serveur, nous constatons l'arrivé d'un nouveau client :
>>> Debut de connexion
Amusons nous à saisir des lignes quelconque dans notre telnet. Nous pouvons constater qu'elle apparaîssent correctement sur la console serveur. Enfin, nous utilisons la chaîne "quit" dans le telnet et le serveut coupe la connexion :

Coté telnet/client :

quit
Connection closed by foreign host.

Coté serveur : quit >>> Fin de connexion Remarquez que le serveur est retourné dans la boucle du premier niveau et attend une nouvelle connexion. Nous pouvons maintenant apporter quelques modification à notre serveur afin, par exemple, que savoir qui s'y connecte. Pour l'heure, nous stockons la valeur renvoyée par la méthode accept dans une variable de type scalaire. Il est également possible de spécifier une liste :

while (my($client, $client_add) = $serveur->accept()) {
Nous récupérons nom seulement l'objet de connexion du client mais également sont adresse IP dans $client_add. Le contenu de cette variable ne peut pas être utilisée tel quel car en fait, il s'agit d'un nom de socket. Un nom de socket est une structure C comprenant l'IP empaqueté et le port. Il nous faut donc, tout d'abord récupérér ces deux éléments avec :
my($port, $empip) = sockaddr_in($client_add);
Nous obtenons le port en clair, mais l'ip n'est toujours pas lisible car il s'agit toujours d'un format interne. Nous devons donc récupérer l'adresse IP sous la forme d'une chaîne avec :
my $clip=inet_ntoa($empip);
Et voilà ! nous avons notre variable $clip contenant la chaîne de caractère correspondante à l'adresse IP du client. Nous pouvons donc réécrire notre boucle comme ceci :
while (my($client, $client_add) = $serveur->accept()) {
	my($port, $empip) = sockaddr_in($client_add);
	my $clip=inet_ntoa($empip);
	print ">>> Debut de connexion avec $clip\n";
	print $client "Salut le client venant de $clip!\n";
	while(my $ligne = <$client>) {
		print $ligne;
		if ($ligne =~ /^QUIT/i ) { close($client); }
		}
	print ">>> Fin de connexion avec $clip\n";
	}
Le client est salué à sont arrivé avec l'adresse IP et nos message sur le serveur nous indique qui se connecte au serveur. Notre serveur est fonctionnel et il nous sera très simple d'ajouter des commandes à interpréter pour qu'il devienne réellement utiles.

2. Client TCP

Telnet est un outil important pour tester le fonctionnement des serveurs. Ceci est valable non seulement pour des serveur en cours de développement mais également pour effectuer des test sur des configuration en production. Il est possible de faire presque tout avec telnet comme par exemple, surfer sur le Web, discuter en IRC, gérer ses mails sur un serveur POP3 ou encore en envoyer en se connectant à un serveur SMPT. Tout cela est possible mais parfaitement hors de question pour une utilisation quotidienne.

Il est donc tout aussi important de scripté pour la partie cliente que pour la partie serveur. La majorité du code que nous venons d'écrire est réutilisable en y apportant quelques modifications.

Comme précédement, nous allons tout d'abord créer notre socket :

my $serveur = IO::Socket::INET->new(
				PeerPort => 6018,
				PeerAddr => '127.0.0.1',
				Type => SOCK_STREAM,
				Proto => 'tcp')
	or die "Impossible de se connecter au serveur : $@\n";
Nous l'aurez devinez, les options à utiliser ne sont pas les même : Le reste du code est beaucoups plus simple que pour le serveur, il nous suffit de lire sur le socket comme avec un descripteur de fichier pour recevoir les informations du serveur et d'utiliser print pour envoyer :
print ">>> Debut de connexion\n";

my $reponse=<$serveur>;
print "Le serveur a dit : $reponse";
print $serveur "quit";

print ">>> Fin de connexion\n";
Nous n'oublions pas de fermer la connexion en fin de script :
close($serveur);
Nous pouvons tester le client sur le serveur :
$ ./client.pl
>>> Debut de connexion
Le serveur a dit : Salut le client venant de 127.0.0.1!
>>> Fin de connexion
Tout va pour le mieux, nous venons d'établir un connection entre deux de nos script Perl. Bien sûr, nos tests on été fait du le localhost mais rien ne vous empêche de procéder à des teste à travers un LAN ou Internet.

3. Forkez le serveur !

Il est extrêmement rare de voir un seul processus envoyer et recevoir des informations à un client. En effet, la programmation d'un serveur complexe devient vite un véritable cauchemar s'il faut en premanence jongler avec les différents descripteur, en particulier, si le serveur accepte plusieurs connexion simultanée.

La bonne solution est alors de cloner le processus serveur originale pour servir le client. Ainsi le processus pere ne traitera jamais les connexion et ne fera qu'écouter le port et se cloner au bon moment.

L'utilisation de l'appel système fork nécessite une petit gymnastique mentale pour ne pas se perdre dans le code au moment de son écriture. En effet, c'est le même code qui sera dupliqué et le programmeur doit composer en conséquence.

Voyons donc une première version de notre nouveau serveur. La première partie du code concernant la création du socket reste indentique :

use IO::Socket;

my $serveur = IO::Socket::INET->new(
				LocalPort => 6018,
				Type => SOCK_STREAM,
				Listen => 1)
	or die "Impossible de devenir serveur : $@\n";

while (my $client = $serveur->accept()) {
Ici, nous arrivons dans la boucle principale. Un connexion est entamée avec un client et la première chose que nous ferons sera de clone le processus :
	next if $pid = fork;
L'instruction next nous permet de sauter un pan de code jusqu'a une occurrence de l'instruction continu. l'appel fork peut retourner plusieurs valeur : undef si l'appel est un échec, 0 si nous somme dans le processus fils et une valeur supérieur à 0 qui correspondont à l'ID du processus (PID) fils si nous somme dans le processus père. Cette ligne nous permet donc de sauter un bloque de code si nous somme le processus père. Les lignes qui vont suivres ne concernent que les processus fils :
	close($serveur);
	print ">>> Debut de connexion sur PID $$\n";
	print $client "Salut le client !\nJe suis le clone PID $$\n";
	while(my $ligne = <$client>) {
		print $ligne;
		if ($ligne =~ /^QUIT/i ) { close($client); }
		}
	print ">>> Fin de connexion sur PID $$\n";
	exit;
La première chose à faire est de fermer notre socket $serveur dont le processus n'a pas besoin. La variable global $$ contient le numéro du processus en cours. Nous affichons ce numéro sur la console et nous l'envoyons au client dans nos salutations. Ceci est purement visuel et nous permet de plus facilement comprendre ce qui se passe. Le processus fils se poursuit normalement comme avec le code précédent jusqu'au moment où le client nous enverra la chaîne "quit". A ce moment, nous fermerons la connexion et quitterons le processus (exit).

Toutes ces lignes seront sauté dans l'exécution du code si nous somme le processus père. Nous arrivons donc à l'instruction :

	} continu {
Comme nous somme le processus père et que nous ne nous chargeons pas de la suite des opération avec les clients, nous pouvons fermez le socket $client et terminé la boucle while. Le processus père revient donc à l'entrée de la boucle et attent une nouvelle connexion.
	close($client);
	}
si vous essayer ce code et que vous surveillez l'état des processus en cours, vous vous apercevrez d'un gros problème :
16852  p5 Z    0:00 (fork_serveur.pl )
Des processus zombi apparaîssent au fils des déconnexion des clients. Un processus zombi n'est pas une catastrophe en soit, il s'agit tout simplement d'un processus mort dont personne n'a relevé la valeur de retour. Un zombi n'occupe pas de ressource CPU ni d'espace autre qu'une entrée inutile dans la table des processus.

Notre présent code génére donc des zombi et cela est parfaitement normal. Le processus père ne cherche à aucun moment à se renseigner sur l'état de ses fils (honteux, n'est-ce pas ?). Pourtant, il recois un signal au moment où ceux-ci meurent : SIGCHLD.

Il nous suffit donc d'ajouter une routine de gestion pour ce signal et de lire la valeur de retour des processus fils pour faire disparaître les zombis.

Perl utilise un hash (table de hachage) pour traiter les signaux et les envoyer vers les routine adéquate : %SIG. La clef de se hash est le nom du signal. Si vous n'avez pas besoins de garder une trace des décès de vos processus fils, vous pouvez utilisez :

$SIG{CHLD} = 'IGNORE';
Il n'y aura pas de zombi puisque le signal est traité dans le processus père, mais si celui-ci n'en fait rien. Si les processus mourant on un dernier souahait, nous allons faire traiter les signaux SIGCHLD par une routine spécifique :
$SIG{CHLD} = \&recupfils;
Cette routine recupfils, nous permettra de récupérer le PID sur fils mort :
sub recupfils {
  my $mort;
  while (($mort = waitpid(-1, WNOHANG)) > 0) {
    print "$mort est mort :(\n";
    }
  $SIG{CHLD} = \&recupfils;
  }
Nous utilisons la fonction waitpid en lui demandant d'attendre n'importe quelle processus (-1) mais de ne pas restée en attente bloquante si aucun processus n'est mort. waitpid retournera un numéro de processus fils qui vient de mourrir et le placera dans $mort. Nous pouvons ensuite utiliser ce numéro, par exemple, pour signaler l'évênement sur la console du serveur.
Petit détail important, l'appel à la fonction waitpid réinstalle le gestionnaire de signaux classique. Nous devons donc modifier à nouveau le hash %SIG pour faire pointer l'entrée correspondante vers notre routine.

Si vous n'avez pas comprit l'ensemble du mécanisme, il vous suffit de tester le nouveau script pour constater le fonctionnement et la gestion des signaux. Notez que la technique consistant à traitée les signaux SIGCHLD de la sorte s'appel moissonner les processus fils morts.

Des parent qui délaissent leur enfants, des fils qui meurent, des moisson d'enfants morts, des pères qui ignore la mort de leur fils et pour couronner le tout, des zombis qui se promennent partout. Décidément, la gestion et le clonage de processus dans un système Unix est un sommet dans l'horreur ;)

Une autre technique pour les serveur en attente de connexion est le clonage par anticipation. Dans ce cas, le processus ne fork par au moment ou les clients se connecte, mais il prepare des copie de lui-même pour attendre les connexions. De ce fait, il y a en premanence sur le système un certain nombre de processus en attente de connexion.
Il faudra faire ce choix si votre serveur doit subir une charge importante. En effet, le fait de cloner le processus au moment de la connexion prend un certain temps à cause du changement de contexte (kernel/user). Si un grand nombre de connexion arrive simultanéement, il y a fort a parier que les temps de réponse seront catastrophique. En préparant un groupe de processus, on répartie cette charge sur autant de clone en attente.

Une dernière solution consiste à ne pas utiliser le fork mais les threads. Les thread sont, en principe plus facile à utilisé et à implémenté mais vous devrez avoir compilé l'interpréteur Perl avec le support multi-thread disponible que depuis la version 5.6.0.

D. Bodor (aka Lefinnois)


Copyright (c) 2001 Linux Magazine France. Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; A copy of the license is included in the section entitled "GNU Free Documentation License".