open()
pour la CIP
perlipc - Communication inter-processus en Perl (signaux, files d'attente, tubes, sous-processus sûrs, sockets et sémaphores)
Les équipements CIP (IPC en anglais, NDT) de base de Perl sont construits sur les signaux, tubes nommés, tubes ouverts, routines de socket de Berkeley et appels CIP du System V, du bon vieil Unix. Chacun est utilisé dans des situations légèrement différentes.
Perl utilise un modèle simple de manipulation des signaux : le hachage %SIG contient les noms des handlers (gestionnaires) de signaux installés par l'utilisateur, ou des références vers eux. Ces handlers seront appelés avec un argument qui est le nom du signal qui l'a déclenché. Un signal peut être généré intentionnellement par une séquence particulière au clavier comme control-C ou control-Z, ou vous être envoyé par un autre processus, ou être déclenché automatiquement par le noyau lorsque des événements spéciaux se produisent, comme la fin d'un processus fils, ou l'épuisement de l'espace disponible sur la pile de votre processus, ou la rencontre d'une limite sur la taille d'un fichier.
Par exemple, pour capturer un signal d'interruption, installez un handler comme ceci. Faites aussi peu de choses que possible dans votre handler ; notez comment nous nous débrouillons pour ne faire que placer une variable globale et lever une exception. C'est parce que sur la plupart des systèmes, les bibliothèques ne sont pas réentrantes, en particulier l'allocation de mémoire et les routines d'entrée/sortie. Cela signifie que pratiquement quoi que ce soit dans votre handler pourrait en théorie provoquer une faute de segmentation et par conséquent un coredump.
sub catch_zap { my $signame = shift; $shucks++; die "Somebody sent me a SIG$signame"; } $SIG{INT} = 'catch_zap'; # pourrait echouer dans un module $SIG{INT} = \&catch_zap; # meilleur strategie
Les noms des signaux sont ceux qui sont listés par kill -l
sur
votre système, ou que vous pouvez obtenir via le module
Config. Installez une liste @signame indexée par numéro pour avoir le
nom et une table %signo indexée par le nom pour avoir le numéro :
use Config; defined $Config{sig_name} || die "No sigs?"; foreach $name (split(' ', $Config{sig_name})) { $signo{$name} = $i; $signame[$i] = $name; $i++; }
Donc pour vérifier si le signal 17 et SIGALRM sont les mêmes, faites juste ceci :
print "signal #17 = $signame[17]\n"; if ($signo{ALRM}) { print "SIGALRM is $signo{ALRM}\n"; }
Vous pouvez aussi choisir d'affecter les chaînes 'IGNORE'
ou
'DEFAULT'
comme handler, dans ce cas Perl essaiera d'ignorer le
signal ou de réaliser l'action par défaut.
Sur la plupart des plateformes Unix, le signal CHLD
(parfois aussi
connu sous le nom CLD
) a un comportement spécial en fonction de la
valeur 'IGNORE'
. Mettre $SIG{CHLD}
à 'IGNORE'
sur une telle
plateforme a pour effet de ne pas créer de processus zombie lorsque le
processus père échoue dans l'attente (wait()
) de son fils (i.e. les
processus fils sont automatiquement détruits). Appeler wait()
avec
$SIG{CHLD}
placé à 'IGNORE'
retourne habituellement -1
sur de
telles plateformes.
Certains signaux ne peuvent être ni piégés ni ignorés, comme les
signaux KILL et STOP (mais pas TSTP). Une stratégie pour ignorer
temporairement des signaux est d'utiliser une déclaration local(), qui
sera automatiquement restaurée à la sortie de votre bloc
d'instructions (Souvenez-vous que les valeurs local()
sont ``héritées''
par les fonctions appelées depuis ce bloc).
sub precious { local $SIG{INT} = 'IGNORE'; &more_functions; } sub more_functions { # interrupts still ignored, for now... }
Envoyer un signal à un identifiant de processus négatif signifie que vous envoyez ce signal à tout le groupe de processus Unix. Ce code envoie un signal hang-up à tous les processus du groupe courant (et place $SIG{HUP} sur IGNORE pour qu'il ne se tue pas lui-même) :
{ local $SIG{HUP} = 'IGNORE'; kill HUP => -$$; # snazzy writing of: kill('HUP', -$$) }
Un autre signal intéressant à envoyer est le signal numéro zéro. Il n'affecte en vérité aucun autre processus, mais vérifie s'il est encore en vie ou a changé son UID.
unless (kill 0 => $kid_pid) { warn "something wicked happened to $kid_pid"; }
Vous pourriez aussi vouloir employer des fonctions anonymes comme handlers de signaux simples :
$SIG{INT} = sub { die "\nOutta here!\n" };
Mais cela sera problématique pour les handlers plus compliqués qui ont
besoin de se réinstaller eux-mêmes. Puisque le mécanisme de
signalisation de Perl est basé sur la fonction signal(3)
de la
bibliothèque C, vous pourriez parfois avoir la malchance d'utiliser
des systèmes où cette fonction est ``cassée'', c'est-à-dire qu'elle se
comporte selon l'ancienne façon peu fiable du SysV plutôt que la plus
récente et plus raisonnable méthode BSD et POSIX. Vous verrez donc les
gens sur la défensive écrire des handlers de signaux ainsi :
sub REAPER { $waitedpid = wait; # maudissez sysV : il nous force non seulement a # reinstaller le handler, mais aussi a le placer # apres le wait $SIG{CHLD} = \&REAPER; } $SIG{CHLD} = \&REAPER; # maintenant, on fait quelque chose qui forke...
Ou même de cette façon plus élaborée :
use POSIX ":sys_wait_h"; sub REAPER { my $child; while (($child = waitpid(-1,WNOHANG)) { $Kid_Status{$child} = $?; } $SIG{CHLD} = \&REAPER; # maudissez encore sysV } $SIG{CHLD} = \&REAPER; # on fait quelque chose qui forke...
La manipulation de signaux est aussi utilisée pour les timeouts sous
Unix. Pendant que vous êtes soigneusement protégé par un bloc
eval{}
, vous installez un handler de signal pour piéger les signaux
d'alarme puis programmez de vous en faire délivrer un dans un certain
nombre de secondes. Puis vous essayez votre opération bloquante,
annulant l'alarme lorsque c'est fait mais pas avant d'être sorti de
votre bloc eval{}
. Si cela ne marche pas, vous utiliserez die()
pour sauter hors du bloc, tout comme vous utiliseriez longjmp()
ou
throw()
dans d'autres langages.
Voici un exemple :
eval { local $SIG{ALRM} = sub { die "alarm clock restart" }; alarm 10; flock(FH, 2); # lock d'ecriture blocante alarm 0; }; if ($@ and $@ !~ /alarm clock restart/) { die }
Si l'opération chronométrée est system()
ou qX(), cette technique peut
générer des zombies. Si cela vous préoccupe, vous devrez faire vos
propres fork()
et exec(), et tuer les processus fils errants.
Pour une manipulation plus complexe des signaux, vous pourriez voir le module du standard POSIX. Il est lamentable que Ceci soit presque entièrement non documenté, mais le fichier t/lib/posix.t de la distribution source de Perl contient quelques exemples.
Un tube nommé (souvent appelé une file d'attente FIFO) est un vieux mécanisme de CIP Unix pour les processus communicant sur la même machine. Il fonctionne tout comme un tube anonyme, normal et connecté, sauf que les processus se donnent rendez-vous en utilisant un nom de fichier et n'ont pas besoin d'être apparentés.
Pour créer un tube nommé, utilisez la commande Unix mknod(1)
ou sur
certains systèmes, mkfifo(1). Elles peuvent ne pas se trouver dans
votre chemin d'accès normal.
# le systeme renvoit les vals a l'envers, donc && et pas || # $ENV{PATH} .= ":/etc:/usr/etc"; if ( system('mknod', $path, 'p') && system('mkfifo', $path) ) { die "mk{nod,fifo} $path failed"; }
Une FIFO est pratique quand vous voulez connecter un processus à un autre sans qu'ils soient apparentés. Lorsque vous ouvrez une FIFO, le programme se bloque jusqu'à ce que quelque chose arrive à l'autre bout.
Par exemple, disons que vous aimeriez que votre fichier .signature soit un tube nommé connecté à un programme en Perl. Désormais, chaque fois qu'un programme (comme un gestionnaire de courrier électronique, un lecteur de news, un finger, etc.) essaye de lire le contenu de ce fichier, il se bloque et votre programme lui fournit une nouvelle signature. Nous utiliserons le test de fichier -p qui teste un tube pour déterminer si quelqu'un (ou quelque chose) a accidentellement supprimé notre FIFO.
chdir; # a la maison $FIFO = '.signature'; $ENV{PATH} .= ":/etc:/usr/games";
while (1) { unless (-p $FIFO) { unlink $FIFO; system('mknod', $FIFO, 'p') && die "can't mknod $FIFO: $!"; }
# la ligne suivante bloque jusqu'a ce qu'il y ait un lecteur open (FIFO, "> $FIFO") || die "can't write $FIFO: $!"; print FIFO "John Smith (smith\@host.org)\n", `fortune -s`; close FIFO; sleep 2; # pour eviter les signaux dupliques }
En installant du code Perl qui traite des signaux, vous vous exposer à
deux types de dangers. D'abord, peu de fonctions de la bibliothèque
système sont réentrantes. Si le signal provoque une interruption
pendant que Perl exécute une fonction (comme malloc(3)
ou printf(3)),
et que votre handler de signal appelle alors la même fonction, vous
pourriez obtenir un comportement non prévisible - souvent, un core
dump. Ensuite, Perl n'est pas lui-même réentrant aux niveaux les plus
bas. Si le signal interrompt Perl pendant qu'il change ses propres
structures de données internes, un comportement non prévisible
similaire peut apparaître.
Vous pouvez faire deux choses, suivant que vous êtes paranoïaque ou
pragmatique. L'approche paranoïaque est d'en faire aussi peu que
possible dans votre handler. Fixez une variable entière existant et
ayant déjà une valeur et revenez. Ceci ne vous aide pas si vous êtes
au milieu d'un appel système lent, qui se contentera de
recommencer. Cela veut dire que vous devez mourir avec die
pour
sauter (longjump(3)) hors du handler. Même ceci est quelque peu
cavalier pour le véritable paranoïaque, qui évite die
dans un
handler car le système est là pour vous attraper. L'approche
pragmatique consiste à dire ``je connais les risques, mais je préfère
la facilité'', et ensuite faire tout ce qu'on veut dans le handler de
signal, en étant prêt à nettoyer les coredumps de temps en temps.
Interdire totalement les handlers interdirait aussi beaucoup de programmes intéressants, y compris virtuellement tout ce qui se trouve dans cette page de manuel, puisque vous ne pourriez plus écrire ne serait-ce qu'un handler SIGCHLD. Ce problème épineux devrait être réglé dans la version 5.005.
open()
pour la CIPl'instruction open()
de base en Perl peut aussi être utilisée pour la
communication inter-processus unidirectionnelle en ajoutant un symbole
de tube juste avant ou après le second argument de open(). Voici
comment démarrer un processus fils dans lequel vous avez l'intention
d'écrire :
open(SPOOLER, "| cat -v | lpr -h 2>/dev/null") || die "can't fork: $!"; local $SIG{PIPE} = sub { die "spooler pipe broke" }; print SPOOLER "stuff\n"; close SPOOLER || die "bad spool: $! $?";
Et voici comment démarrer un processus fils depuis lequel vous désirez lire :
open(STATUS, "netstat -an 2>&1 |") || die "can't fork: $!"; while (<STATUS>) { next if /^(tcp|udp)/; print; } close STATUS || die "bad netstat: $! $?";
Si l'on peut etre certain qu'un programme specifique est un script Perl que attend des noms de fichiers dans @ARGV, le programmeur futé peut écrire quelque chose comme ceci :
% program f1 "cmd1|" - f2 "cmd2|" f3 < tmpfile
et, indépendemment du shell d'où il est appelé, le programme Perl lira dans le fichier f1, le processus cmd1, l'entrée standard (tmpfile dans ce cas), le fichier f2, la commande cmd2, et finalement dans le fichier f3. Plutôt débrouillard, hein ?
Vous pourriez remarquer que l'on pourrait utiliser des accents graves pour obtenir à peu près le même effet que l'ouverture d'un tube en lecture :
print grep { !/^(tcp|udp)/ } `netstat -an 2>&1`; die "bad netstat" if $?;
Même si ceci est vrai en surface, il est bien plus efficace de traiter le fichier une ligne ou un enregistrement à la fois car alors vous n'avez pas à charger toute la chose en mémoire d'un seul coup. Cela vous donne aussi un contrôle plus fin sur tout le processus, vous laissant tuer le processus fils plus tôt si vous le voulez.
Prenez soin de vérifier à la fois les valeurs de retour de open()
et
de close(). Si vous écrivez dans un tube, vous pouvez aussi piéger
SIGPIPE. Sinon, pensez à ce qui se passe lorsque vous ouvrez un tube
vers une commande qui n'existe pas : le open()
réussira très
probablement (il ne fait que refléter le succès du fork()), mais
ensuite votre sortie échouera - spectaculairement. Perl ne peut pas
savoir si la commande a fonctionné parce que votre commande tourne en
fait dans un processus séparé dont l'exec() peut avoir échoué. Par
conséquent, ceux qui lisent depuis une commande bidon retournent juste
une rapide fin de fichier, ceux qui écrivent vers une commande bidon
provoqueront un signal qu'ils feraient mieux d'être préparés à
gérer. Considérons :
open(FH, "|bogus") or die "can't fork: $!"; print FH "bang\n" or die "can't write: $!"; close FH or die "can't close: $!";
Ceci n'explosera pas avant le close, et ce sera sur un SIGPIPE. Pour l'intercepter, vous pourriez utiliser ceci :
$SIG{PIPE} = 'IGNORE'; open(FH, "|bogus") or die "can't fork: $!"; print FH "bang\n" or die "can't write: $!"; close FH or die "can't close: status=$?";
Le processus principal et tous les processus fils qu'il peut générer partagent les mêmes handles de fichiers STDIN, STDOUT et STDERR. Si deux processus essayent d'y accéder en même temps, des choses étranges peuvent se produire. Vous pourriez aussi fermer ou réouvrir les handles de fichiers pour le fils. Vous pouvez contourner cela en ouvrant votre tube avec open(), mais sur certains systèmes cela signifie que le processus fils ne peut pas survivre à son père.
Vous pouvez exécuter une commande en arrière-plan avec :
system("cmd &");
Les STDOUT et STDERR de la commande (et peut-être STDIN, selon votre shell) seront les mêmes que ceux du père. Vous n'aurez pas besoin de piéger SIGCHLD à cause du double fork qui se produit (voir plus bas pour plus de détails).
Dans certains cas (démarrage de processus serveurs, par exemple) vous
voudrez complètement dissocier le processus fils de son père. Ceci est
souvent appelé une démonisation. Un démon bien élevé fera aussi un
chdir()
vers le répertoire root (pour qu'il n'empêche pas le démontage
du système de fichiers contenant le répertoire à partir duquel il a
été lancé) et redirige ses descripteurs de fichier standard vers
/dev/null (pour que des sorties aléatoires ne finissent pas sur le
terminal de l'utilisateur).
use POSIX 'setsid';
sub daemonize { chdir '/' or die "Can't chdir to /: $!"; open STDIN, '/dev/null' or die "Can't read /dev/null: $!"; open STDOUT, '>/dev/null' or die "Can't write to /dev/null: $!"; defined(my $pid = fork) or die "Can't fork: $!"; exit if $pid; setsid or die "Can't start a new session: $!"; open STDERR, '>&STDOUT' or die "Can't dup stdout: $!"; }
Le fork()
doit venir avant le setsid()
pour s'assurer que vous n'êtes
pas un leader de groupe de processus (le setsid()
échouera si vous
l'êtes). Si votre système ne possède pas la fonction setsid(), ouvrez
/dev/tty et utilisez dessus l'ioctl() TIOCNOTTY
à la place.
Voir tty(4) pour plus de détails.
Les utilisateurs non unixiens devraient jeter un oeil sur leur module Votre_OS::Process pour avoir d'autres solutions.
Une autre approche intéressante de la CIP est de rendre votre simple
programme multiprocessus et de communiquer entre (ou même parmi)
eux. La fonction open()
acceptera un fichier en argument pour "-|"
ou "|-"
afin de faire une chose très intéressante : elle forkera un
fils connecté au descripteur de fichier que vous venez d'ouvrir. Le
fils exécute le même programme que le parent. C'est utile par exemple
pour ouvrir de façon sécurisée un fichier lorsque l'on s'exécute sous
un UID ou un GID présumé. Si vous ouvrez un tube vers minus, vous
pouvez écrire dans le descripteur de fichier que vous avez ouvert et
votre fils le trouvera dans son STDIN. Si vous ouvrez un tube
depuis minus, vous pouvez lire depuis le descripteur de fichier
tout ce que votre fils écrit dans son STDOUT.
use English; my $sleep_count = 0;
do { $pid = open(KID_TO_WRITE, "|-"); unless (defined $pid) { warn "cannot fork: $!"; die "bailing out" if $sleep_count++ > 6; sleep 10; } } until defined $pid;
if ($pid) { # parent print KID_TO_WRITE @some_data; close(KID_TO_WRITE) || warn "kid exited $?"; } else { # child ($EUID, $EGID) = ($UID, $GID); # suid progs only open (FILE, "> /safe/file") || die "can't open /safe/file: $!"; while (<STDIN>) { print FILE; # child's STDIN is parent's KID } exit; # n'oubliez pas ceci }
Un autre usage courant de cette construction apparaît lorsque vous
avez besoin d'exécuter quelque chose sans interférences de la part du
shell. Avec system(), c'est direct, mais vous ne pouvez pas utiliser
un tube ouvert ou des accents graves de façon sûre. C'est parce qu'il
n'y a pas de moyen d'empêcher le shell de mettre son nez dans vos
arguments. À la place, utilisez le contrôle de bas niveau pour appeler
exec()
directement.
Voici un backtick ou un tube ouvert en lecture sûrs :
# ajoutez un traitement d'erreur comme ci-dessus $pid = open(KID_TO_READ, "-|");
if ($pid) { # parent while (<KID_TO_READ>) { # faites quelque chose d'intéressant } close(KID_TO_READ) || warn "kid exited $?";
} else { # fils ($EUID, $EGID) = ($UID, $GID); # suid uniquement exec($program, @options, @args) || die "can't exec program: $!"; # PASATTEINT }
Et voici un tube sûr ouvert en écriture :
# ajoutez un traitement d'erreur comme ci-dessus $pid = open(KID_TO_WRITE, "|-"); $SIG{ALRM} = sub { die "whoops, $program pipe broke" };
if ($pid) { # parent for (@data) { print KID_TO_WRITE; } close(KID_TO_WRITE) || warn "kid exited $?";
} else { # fils ($EUID, $EGID) = ($UID, $GID); exec($program, @options, @args) || die "can't exec program: $!"; # PASATTEINT }
Notez que ces opérations sont des forks Unix complets, ce qui signifie qu'ils peuvent ne pas être correctement implémentés sur des systèmes étrangers. De plus, il ne s'agit pas d'un vrai multithreading. Si vous désirez en apprendre plus sur le threading, voyez le fichier modules mentionnée plus bas dans la section VOIR AUSSI.
Même si ceci fonctionne raisonnablement bien pour la communication unidirectionnelle, qu'en est-il pour la communication bidirectionnelle ? La chose la plus évidente que vous aimeriez faire ne marche en fait pas :
open(PROG_FOR_READING_AND_WRITING, "| un programme |")
et si vous oubliez d'utiliser le pragma use warnings
ou l'option
-w, alors vous raterez complètement le message de diagnostic :
Can't do bidirectional pipe at -e line 1.
Si vous le voulez vraiment, vous pouvez utiliser la fonction de la
bibliothèque standard open2()
pour attraper les deux bouts. Il existe
aussi une open3()
pour les E/S tridirectionnelles de façon que vous
puissiez aussi récupérer le STDERR de votre fils, mais faire ceci
nécessiterait une boucle select()
maladroite et ne vous permettrait
pas d'utiliser les opérations d'entrée normales de Perl.
Si vous jetez un oeil sur son source, vous verrez que open2()
utilise
des primitives de bas niveau, comme les appels pipe()
et exec()
d'Unix, pour créer toutes les connexions. Il aurait peut-être été un
peu plus efficace d'utiliser socketpair(), mais cela serait devenu
encore moins portable que ce ne l'est déjà. Les fonctions open2()
et
open3()
ont peu de chances de fonctionner ailleurs que sur un système
Unix ou quelques autres prétendant être conformes à la norme POSIX.
Voici un exemple d'utilisation de open2():
use FileHandle; use IPC::Open2; $pid = open2(*Reader, *Writer, "cat -u -n" ); print Writer "stuff\n"; $got = <Reader>;
Le problème avec ceci est que le buffering d'Unix va vraiment rendre
cette opération pénible. Même si votre descripteur de fichier Writer
est
vidé automatiquement, et même si le processus à l'autre bout reçoit
vos données de façon régulière, vous ne pouvez habituellement rien
faire pour le forcer à vous les renvoyer rapidement de la même
façon. Dans ce cas-ci, nous le pouvons, car nous avons donné à cat
une option -u pour qu'il n'ait pas de tampon. Mais très peu de
commandes Unix sont conçues pour fonctionner à travers des tubes, ceci
marche donc rarement à moins que vous ayez écrit vous-mêmes le
programme se trouvant à l'autre bout du tube à deux sens.
Une solution à ceci est la bibliothèque non standard Comm.pl. Elle utilise des pseudo-ttys pour rendre le comportement de votre programme plus raisonnable :
require 'Comm.pl'; $ph = open_proc('cat -n'); for (1..10) { print $ph "a line\n"; print "got back ", scalar <$ph>; }
De cette façon vous n'avez pas besoin de contrôler le code source du
programme que vous utilisez. La bibliothèque Comm contient aussi
les fonctions expect()
et interact(). Vous trouverez la bibliothèque
(et, nous l'espérons, son successeur IPC::Chat) dans l'archive CPAN
la plus proche de vous, comme il est détaillé dans la section VOIR
AUSSI ci-dessous.
Le module plus récent Expect.pm sur le CPAN s'occupe aussi de ce genre de choses. Ce module nécessite deux autres modules du CPAN : IO::Pty et IO::Stty. Il installe un pseudo-terminal pour interagir avec les programmes qui insistent pour discuter avec le pilote de périphérique du terminal. Si votre système fait partie de ceux supportés, cela pourrait être la meilleure solution pour vous.
Si vous voulez, vous pouvez faire des pipe()
et des fork()
de bas
niveau pour coudre tout cela ensemble à la main. Cet exemple ne se
parle qu'à lui-même, mais vous pourriez réouvrir les handles
appropriés vers STDIN et STDOUT et appeler d'autres processus.
#!/usr/bin/perl -w # pipe1 - communication bidirectionnelle utilisant deux paires # de tubes concus pour les systèmes n'ayant pas l'appel socketpair() use IO::Handle; # milliers de lignes juste pour l'autoflush :-( pipe(PARENT_RDR, CHILD_WTR); # XXX: echec ? pipe(CHILD_RDR, PARENT_WTR); # XXX: echec ? CHILD_WTR->autoflush(1); PARENT_WTR->autoflush(1);
if ($pid = fork) { close PARENT_RDR; close PARENT_WTR; print CHILD_WTR "Parent Pid $$ is sending this\n"; chomp($line = <CHILD_RDR>); print "Parent Pid $$ just read this: `$line'\n"; close CHILD_RDR; close CHILD_WTR; waitpid($pid,0); } else { die "cannot fork: $!" unless defined $pid; close CHILD_RDR; close CHILD_WTR; chomp($line = <PARENT_RDR>); print "Child Pid $$ just read this: `$line'\n"; print PARENT_WTR "Child Pid $$ is sending this\n"; close PARENT_RDR; close PARENT_WTR; exit; }
Mais vous n'avez en fait pas besoin de faire deux appels à des tubes. Si vous disposez de l'appel système socketpair(), il fera tout cela pour vous.
#!/usr/bin/perl -w # pipe2 - communication bidirectionnelle utilisant socketpair # "les choses partagées dans les deux sens sont toujours les meilleures"
use Socket; use IO::Handle; # milliers de lignes juste pour l'autoflush :-( # Nous utilisons AF_UNIX car bien que *_LOCAL est la forme # POSIX 1003.1g de la constante, beaucoup de machines ne # l'ont pas encore. socketpair(CHILD, PARENT, AF_UNIX, SOCK_STREAM, PF_UNSPEC) or die "socketpair: $!";
CHILD->autoflush(1); PARENT->autoflush(1);
if ($pid = fork) { close PARENT; print CHILD "Parent Pid $$ is sending this\n"; chomp($line = <CHILD>); print "Parent Pid $$ just read this: `$line'\n"; close CHILD; waitpid($pid,0); } else { die "cannot fork: $!" unless defined $pid; close CHILD; chomp($line = <PARENT>); print "Child Pid $$ just read this: `$line'\n"; print PARENT "Child Pid $$ is sending this\n"; close PARENT; exit; }
Même si elles ne sont pas limitées aux systèmes d'exploitation dérivés d'Unix (e.g., WinSock sur les PC fournit le support des sockets, tout comme le font les bibliothèques VMS), vous ne disposez peut-être pas des sockets sur votre système, auquel cas cette section ne vous fera probablement pas beaucoup de bien. Avec les sockets, vous pouvez à la fois créer des circuits virtuels (i.e., des streams TCP) et des datagrammes (i.e., des paquets UDP). Vous pouvez même peut-être faire plus, en fonction de votre système.
Les appels Perl pour traiter les sockets ont les mêmes noms que les appels système correspondants en C, mais leurs arguments tendent à être différents pour deux raisons : tout d'abord, les handles de fichiers de Perl fonctionnent différemment des descripteurs de fichiers en C. Ensuite, Perl connaît déjà la longueur de ses chaînes, vous n'avez donc pas besoin de passer cette information.
L'un des problèmes majeurs avec l'ancien code des sockets de Perl
était qu'il utilisait des valeurs codées en dur pour certaines des
constantes, ce qui endommageait sévèrement la portabilité. Si vous
avez déjà vu du code qui fait des choses comme fixer explicitement
$AF_INET = 2
, vous savez que vous avez un gros problème : une
approche incommensurablement supérieure est d'utiliser le module
Socket
, qui fournit un accès plus fiable aux différentes constantes
et fonctions dont vous aurez besoin.
Si vous n'êtes pas en train d'écrire un couple serveur/client pour un protocole existant comme NNTP or SMTP, vous devriez réfléchir à la façon dont votre serveur saura quand le client a fini de parler, et vice-versa. La plupart des protocoles sont basés sur des messages et des réponses d'une ligne (de façon que l'une des parties sache que l'autre a fini quand un ``\n'' est reçu) ou sur des messages et des réponses de plusieurs lignes qui se finissent par un point ou une ligne vide (``\n.\n'' termine un message ou une réponse).
Le terminateur de ligne de l'Internet est ``\015\012''. Sous certaines variantes ASCII d'Unix, cela peut habituellement être écrit ``\r\n'', mais sous certains autres systèmes, ``\r\n'' peut certaines fois être ``\015\015\012'', ``\012\012\015'', ou quelque chose de complètement différent. Les standards spécifient d'écrire ``\015\012'' pour être conforme (soyez strict sur ce que vous fournissez), mais ils recommandent aussi d'accepter un ``\012'' solitaire en entrée (soyez indulgent sur ce que vous attendez). Nous n'avons pas toujours été très bon à ce sujet dans le code de cette page de manuel, mais à moins que vous ne soyez sur un Mac, cela devrait aller.
Utilisez des sockets Internet lorsque vous voulez effectuer une communication client-serveur qui pourrait s'étendre à des machines hors de votre propre système.
Voici un exemple de client TCP utilisant des sockets Internet :
#!/usr/bin/perl -w use strict; use Socket; my ($remote,$port, $iaddr, $paddr, $proto, $line);
$remote = shift || 'localhost'; $port = shift || 2345; # port pris au hasard if ($port =~ /\D/) { $port = getservbyname($port, 'tcp') } die "No port" unless $port; $iaddr = inet_aton($remote) || die "no host: $remote"; $paddr = sockaddr_in($port, $iaddr);
$proto = getprotobyname('tcp'); socket(SOCK, PF_INET, SOCK_STREAM, $proto) || die "socket: $!"; connect(SOCK, $paddr) || die "connect: $!"; while (defined($line = <SOCK>)) { print $line; }
close (SOCK) || die "close: $!"; exit;
Et voici le serveur correspondant pour l'accompagner. Nous laisserons l'adresse sous la forme INADDR_ANY pour que le noyau puisse choisir l'interface appropriée sur les hôtes à plusieurs pattes. Si vous voulez rester sur une interface particulière (comme le côté externe d'une passerelle ou d'un firewall), vous devriez la remplacer par votre véritable adresse.
#!/usr/bin/perl -Tw use strict; BEGIN { $ENV{PATH} = '/usr/ucb:/bin' } use Socket; use Carp; $EOL = "\015\012";
sub logmsg { print "$0 $$: @_ at ", scalar localtime, "\n" }
my $port = shift || 2345; my $proto = getprotobyname('tcp'); $port = $1 if $port =~ /(\d+)/; # nettoie le numero de port
socket(Server, PF_INET, SOCK_STREAM, $proto) || die "socket: $!"; setsockopt(Server, SOL_SOCKET, SO_REUSEADDR, pack("l", 1)) || die "setsockopt: $!"; bind(Server, sockaddr_in($port, INADDR_ANY)) || die "bind: $!"; listen(Server,SOMAXCONN) || die "listen: $!";
logmsg "server started on port $port";
my $paddr;
$SIG{CHLD} = \&REAPER;
for ( ; $paddr = accept(Client,Server); close Client) { my($port,$iaddr) = sockaddr_in($paddr); my $name = gethostbyaddr($iaddr,AF_INET);
logmsg "connection from $name [", inet_ntoa($iaddr), "] at port $port";
print Client "Hello there, $name, it's now ", scalar localtime, $EOL; }
Et voici une version multithread. Elle est multithread en ce sens que comme la plupart des serveurs, elle génère (fork) un serveur esclave pour manipuler la requête du client de façon que le serveur maître puisse rapidement revenir servir un nouveau client.
#!/usr/bin/perl -Tw use strict; BEGIN { $ENV{PATH} = '/usr/ucb:/bin' } use Socket; use Carp; $EOL = "\015\012";
sub spawn; # déclaration préalable sub logmsg { print "$0 $$: @_ at ", scalar localtime, "\n" }
my $port = shift || 2345; my $proto = getprotobyname('tcp'); $port = $1 if $port =~ /(\d+)/; # nettoie le numéro de port
socket(Server, PF_INET, SOCK_STREAM, $proto) || die "socket: $!"; setsockopt(Server, SOL_SOCKET, SO_REUSEADDR, pack("l", 1)) || die "setsockopt: $!"; bind(Server, sockaddr_in($port, INADDR_ANY)) || die "bind: $!"; listen(Server,SOMAXCONN) || die "listen: $!";
logmsg "server started on port $port";
my $waitedpid = 0; my $paddr;
sub REAPER { $waitedpid = wait; $SIG{CHLD} = \&REAPER; # abhorrons sysV logmsg "reaped $waitedpid" . ($? ? " with exit $?" : ''); }
$SIG{CHLD} = \&REAPER;
for ( $waitedpid = 0; ($paddr = accept(Client,Server)) || $waitedpid; $waitedpid = 0, close Client) { next if $waitedpid and not $paddr; my($port,$iaddr) = sockaddr_in($paddr); my $name = gethostbyaddr($iaddr,AF_INET);
logmsg "connection from $name [", inet_ntoa($iaddr), "] at port $port";
spawn sub { print "Hello there, $name, it's now ", scalar localtime, $EOL; exec '/usr/games/fortune' # XXX: `mauvais' terminateurs de ligne or confess "can't exec fortune: $!"; };
}
sub spawn { my $coderef = shift;
unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') { confess "usage: spawn CODEREF"; }
my $pid; if (!defined($pid = fork)) { logmsg "cannot fork: $!"; return; } elsif ($pid) { logmsg "begat $pid"; return; # Je suis le pere } # ou bien je suis le fils - on se multiplie
open(STDIN, "<&Client") || die "can't dup client to stdin"; open(STDOUT, ">&Client") || die "can't dup client to stdout"; ## open(STDERR, ">&STDOUT") || die "can't dup stdout to stderr"; exit &$coderef(); }
Ce serveur prend la peine de cloner un processus fils via fork()
pour
chaque requête entrante. De cette façon, il peut répondre à de
nombreuses requêtes en même temps, ce que vous pourriez ne pas
toujours désirer. Même si vous n'utilisez pas fork(), le listen()
permettra de nombreuses connexions en cours. Les serveurs qui se
dupliquent doivent prendre tout particulièrement soin de nettoyer
leurs enfants morts (appelés ``zombies'' en jargon unixien), car
autrement vous remplirez rapidement votre table des processus.
Nous vous suggérons d'utiliser l'option -T pour profiter du taint checking (voir la page de manuel perlsec) même si vous ne tourner pas en setuid ou en setgid. C'est toujours une bonne idée pour les serveurs et les programmes exécutés au nom de quelqu'un d'autre (comme les scripts CGI), car cela diminue le risque que des personnes extérieures compromettent votre système.
Étudions un autre client TCP. Celui-ci se connecte au service TCP ``time'' sur de nombreuses machines différentes et montre à quel point leurs horloges diffèrent du système sur lequel il est exécuté :
#!/usr/bin/perl -w use strict; use Socket;
my $SECS_of_70_YEARS = 2208988800; sub ctime { scalar localtime(shift) }
my $iaddr = gethostbyname('localhost'); my $proto = getprotobyname('tcp'); my $port = getservbyname('time', 'tcp'); my $paddr = sockaddr_in(0, $iaddr); my($host);
$| = 1; printf "%-24s %8s %s\n", "localhost", 0, ctime(time());
foreach $host (@ARGV) { printf "%-24s ", $host; my $hisiaddr = inet_aton($host) || die "unknown host"; my $hispaddr = sockaddr_in($port, $hisiaddr); socket(SOCKET, PF_INET, SOCK_STREAM, $proto) || die "socket: $!"; connect(SOCKET, $hispaddr) || die "bind: $!"; my $rtime = ' '; read(SOCKET, $rtime, 4); close(SOCKET); my $histime = unpack("N", $rtime) - $SECS_of_70_YEARS ; printf "%8d %s\n", $histime - time, ctime($histime); }
C'est bien pour les clients et les serveurs Internet, mais pour les communications locales ? Même si vous pouvez utiliser la même configuration, vous ne le voulez pas toujours. Les sockets du domaine Unix sont locales à l'hôte courant, et sont souvent utilisées en interne pour implémenter des tubes. Contrairement aux sockets du domaine Internet, les sockets Unix peuvent se voir dans le système de fichier par ls(1).
% ls -l /dev/log srw-rw-rw- 1 root 0 Oct 31 07:23 /dev/log
Vous pouvez les tester avec le test de fichier -S de Perl :
unless ( -S '/dev/log' ) { die "something's wicked with the print system"; }
Voici un exemple de client du domaine Unix :
#!/usr/bin/perl -w use Socket; use strict; my ($rendezvous, $line);
$rendezvous = shift || '/tmp/catsock'; socket(SOCK, PF_UNIX, SOCK_STREAM, 0) || die "socket: $!"; connect(SOCK, sockaddr_un($rendezvous)) || die "connect: $!"; while (defined($line = <SOCK>)) { print $line; } exit;
Et voici un serveur correspondant. Vous n'avez pas besoin de vous soucier ici des stupides terminateurs de réseau car les sockets Unix sont de façon certaine sur l'hôte local (localhost, NDT), et ainsi tout fonctionne bien.
#!/usr/bin/perl -Tw use strict; use Socket; use Carp;
BEGIN { $ENV{PATH} = '/usr/ucb:/bin' } sub logmsg { print "$0 $$: @_ at ", scalar localtime, "\n" }
my $NAME = '/tmp/catsock'; my $uaddr = sockaddr_un($NAME); my $proto = getprotobyname('tcp');
socket(Server,PF_UNIX,SOCK_STREAM,0) || die "socket: $!"; unlink($NAME); bind (Server, $uaddr) || die "bind: $!"; listen(Server,SOMAXCONN) || die "listen: $!";
logmsg "server started on $NAME";
my $waitedpid;
sub REAPER { $waitedpid = wait; $SIG{CHLD} = \&REAPER; # maudissez sysV logmsg "reaped $waitedpid" . ($? ? " with exit $?" : ''); }
$SIG{CHLD} = \&REAPER;
for ( $waitedpid = 0; accept(Client,Server) || $waitedpid; $waitedpid = 0, close Client) { next if $waitedpid; logmsg "connection on $NAME"; spawn sub { print "Hello there, it's now ", scalar localtime, "\n"; exec '/usr/games/fortune' or die "can't exec fortune: $!"; }; }
Comme vous le voyez, il est remarquablement similaire au serveur TCP
Internet, à tel point que, en fait, nous avons omis plusieurs
fonctions identiques - spawn(), logmsg(), ctime(), et REAPER()
- qui
sont exactement les mêmes que dans l'autre serveur.
Alors pourquoi vouloir utiliser une socket du domaine Unix au lieu
d'un tube nommé plus simple ? Parce qu'un tube nommé ne vous procure
pas de sessions. Vous ne pouvez pas différencier les données d'un
processus de celle d'un autre. Avec la programmation de sockets, vous
obtenez une session distincte pour chaque client : c'est pourquoi
accept()
prend deux arguments.
Par exemple, supposons que vous voulez donner accès à un démon serveur de base de données tournant depuis longtemps à des copains sur le web, mais seulement s'ils passent par une interface CGI. Vous aurez un programme CGI petit et simple qui fera toutes les vérifications et les connexions que vous voudrez, puis agira comme un client du domaine Unix et se connectera à votre serveur privé.
Pour ceux qui préfèrent une interface de plus haut niveau pour la programmation des sockets, le module IO::Socket fournit une approche orientée objet. IO::Socket fait partie de la distribution standard de Perl à partir de la version 5.004. Si vous exploitez une version précédente de Perl, récupérez juste IO::Socket sur le CPAN, où vous trouverez aussi des modules fournissant des interfaces simples pour les systèmes suivants : DNS, FTP, Ident (RFC 931), NIS et NISPlus, NNTP, Ping, POP3, SMTP, SNMP, SSLeay, Telnet, et Time - pour n'en citer que quelques-uns.
Voici un client qui crée une connexion TCP avec le service ``daytime'' sur le port 13 de l'hôte appelé ``localhost'' et affiche tout ce que le serveur veut bien fournir.
#!/usr/bin/perl -w use IO::Socket; $remote = IO::Socket::INET->new( Proto => "tcp", PeerAddr => "localhost", PeerPort => "daytime(13)", ) or die "cannot connect to daytime port at localhost"; while ( <$remote> ) { print }
Lorsque vous lancez ce programme, vous devriez obtenir quelque chose qui ressemble à ceci :
Wed May 14 08:40:46 MDT 1997
Voici ce que signifient les paramètres du constructeur new
:
Proto
PeerAddr
"www.perl.com"
, ou une adresse comme "204.148.40.9"
. Pour les
besoins de la démonstration, nous avons utilisé le nom d'hôte spécial
"localhost"
, qui doit toujours désigner la machine sur laquelle le
programme tourne. L'adresse Internet correspondant à localhost est
"127.1"
, si vous préférez l'utiliser.
PeerPort
"daytime"
sur les systèmes ayant un fichier de services système
bien configuré, [FOOTNOTE: Le fichier de services système est dans
/etc/services sous Unix] mais juste au cas où, nous avons spécifié
le numéro de port (13) entre parenthèses. La simple utilisation du
numéro aurait aussi fonctionné, mais les numéros constants rendent
nerveux les programmeurs soigneux.
Vous avez remarqué comment la valeur de retour du constructeur new
est utilisée comme descripteur de fichier dans la boucle while
?
C'est ce qu'on appelle un descripteur de fichier indirect, une
variable scalaire contenant un descripteur de fichier. Vous pouvez
l'utiliser de la même façon qu'un descripteur normal. Par exemple,
vous pouvez y lire une ligne de la manière suivante :
$line = <$handle>;
toutes les lignes restantes de cette façon :
@lines = <$handle>;
et y écrire une ligne ainsi :
print $handle "some data\n";
Voici un client simple qui prend en paramètre un hôte distant, puis une liste de documents à récupérer sur cet hôte. C'est un client plus intéressant que le précédent car il commence par envoyer quelque chose avant de récupérer la réponse du serveur,
#!/usr/bin/perl -w use IO::Socket; unless (@ARGV > 1) { die "usage: $0 host document ..." } $host = shift(@ARGV); $EOL = "\015\012"; $BLANK = $EOL x 2; foreach $document ( @ARGV ) { $remote = IO::Socket::INET->new( Proto => "tcp", PeerAddr => $host, PeerPort => "http(80)", ); unless ($remote) { die "cannot connect to http daemon on $host" } $remote->autoflush(1); print $remote "GET $document HTTP/1.0" . $BLANK; while ( <$remote> ) { print } close $remote; }
le serveur web gérant le service ``http'', qui est supposé être à son numéro de
port standard, le 80. Si le serveur auquel vous essayez de vous connecter est
à un numéro de port différent (comme 1080 ou 8080), vous devez le spécifier
par le paramètre nommé PeerPort => 8080
. La méthode autoflush
est
utilisée sur la socket car autrement le système placerait toutes les sorties
que nous y envoyons dans un buffer (si vous êtes sur un Mac, vous devrez aussi
remplacer tous les "\n"
dans votre code qui envoie des données sur le
réseau par des "\015\012"
.)
Se connecter au serveur est seulement la première partie du travail : une fois que vous avez la connexion, vous devez utiliser le langage du serveur. Chaque serveur sur le réseau à son propre petit langage de commande, qu'il attend en entrée. La chaîne que nous envoyons au serveur et qui commence par ``GET'' est dans la syntaxe HTTP. Dans ce cas, nous demandons simplement chaque document spécifié. Oui, nous créons vraiment une nouvelle connexion pour chaque document, même s'ils se trouvent tous sur le même hôte. Vous avez toujours parlé HTTP de cette façon. Les versions les plus récentes des clients web peuvent demander au serveur distant de laisser la connexion ouverte un petit moment, mais le serveur n'est pas tenu d'honorer une telle requête.
Voici un exemple d'exécution de ce programme, que nous appellerons webget :
% webget www.perl.com /guanaco.html HTTP/1.1 404 File Not Found Date: Thu, 08 May 1997 18:02:32 GMT Server: Apache/1.2b6 Connection: close Content-type: text/html
<HEAD><TITLE>404 File Not Found</TITLE></HEAD> <BODY><H1>File Not Found</H1> The requested URL /guanaco.html was not found on this server.<P> </BODY>
Ok, ce n'est donc pas très intéressant, car il n'a pas trouvé ce document en particulier. Mais une longue réponse n'aurait pas tenu sur cette page.
Pour avoir une version un peu plus développée de ce programme, vous devriez jeter un oeil sur le programme lwp-request inclus dans les modules LWP du CPAN.
Bien, ceci est parfait si vous voulez envoyer une commande et obtenir une réponse, mais pourquoi ne pas mettre en place quelque chose de pleinement interactif, un peu à la façon dont telnet fonctionne ? Ainsi vous pouvez taper une ligne, obtenir la réponse, taper une autre ligne, avoir la réponse, etc.
Ce client est plus compliqué que les deux que nous avons écrits
jusqu'à maintenant, mais si vous êtes sur un système qui supporte le
puissant appel fork
, la solution n'est pas si rude que ça. Une fois
que vous avez obtenu la connexion au service avec lequel vous désirez
discuter, appelez fork
pour cloner votre processus. Chacun de ces
deux processus identiques à un travail très simple à réaliser : le
parent copie tout ce qui provient de la socket sur la sortie standard,
pendant que le fils copie simultanément sur la socket tout ce qui
vient de l'entrée standard. Accomplir la même chose en utilisant un
seul processus serait bien plus difficile, car il est plus facile
de coder deux processus qui font une seule chose, qu'un seul processus
qui en fait deux (Ce principe de simplicité est une pierre angulaire
de la philosophie Unix, ainsi que du bon génie logiciel, c'est
probablement pour cela qu'il s'est étendu à d'autres systèmes).
Voici le code:
#!/usr/bin/perl -w use strict; use IO::Socket; my ($host, $port, $kidpid, $handle, $line);
unless (@ARGV == 2) { die "usage: $0 host port" } ($host, $port) = @ARGV;
# cree une connexion tcp a l'hote et sur le port specifie $handle = IO::Socket::INET->new(Proto => "tcp", PeerAddr => $host, PeerPort => $port) or die "can't connect to port $port on $host: $!";
$handle->autoflush(1); # pour que la sortie y aille tout de suite print STDERR "[Connected to $host:$port]\n";
# coupe le programme en deux processus, jumeaux identiques die "can't fork: $!" unless defined($kidpid = fork());
# le bloc if{} n'est traverse que dans le processus pere if ($kidpid) { # copie la socket sur la sortie standard while (defined ($line = <$handle>)) { print STDOUT $line; } kill("TERM", $kidpid); # envoie SIGTERM au fils } # le bloc else{} n'est traverse que dans le fils else { # copie l'entree standard sur la socket while (defined ($line = <STDIN>)) { print $handle $line; } }
La fonction kill
dans le bloc if
du père est là pour envoyer un
signal à notre processus fils (qui est dans le bloc else
) dès que
le serveur distant a fermé la connexion de son côté.
Si le serveur distant envoie les données un octet à la fois, et que
vous avez besoin de ces données immédiatement sans devoir attendre une
fin de ligne (qui peut très bien ne jamais arriver), vous pourriez
remplacer la boucle while
dans le père par ce qui suit :
my $byte; while (sysread($handle, $byte, 1) == 1) { print STDOUT $byte; }
Faire un appel système pour chaque octet que vous voulez lire n'est pas très efficace (c'est le moins qu'on puisse dire) mais c'est le plus simple à expliquer et cela fonctionne raisonnablement bien.
Comme toujours, mettre en place un serveur est un peu plus exigeant
que de faire tourner un client. Le modèle est que le serveur crée un
type de socket spécial qui ne fait rien à part écouter un port
particulier pour attendre l'arrivée d'une connexion. Il le fait en
appelant la méthode IO::Socket::INET->new()
avec des arguments
légèrement différents de ceux du client.
"tcp"
ici.
LocalPort
, ce que
nous n'avons pas fait pour le client. C'est le nom du service ou le
numéro du port dont vous voulez être le serveur (sous Unix, les ports
en dessous de 1024 sont réservés au superutilisateur). Dans notre
exemple, nous utiliserons le port 9000, mais vous pouvez utiliser
n'importe quel port non utilisé couramment sur votre système. Si vous
essayez d'en utiliser un qui soit déjà exploité, vous obtiendrez le
message ``Address already in use''. Sous Unix, la commande netstat -a
vous montrera quels services ont des serveurs en cours.
Listen
détermine le nombre maximal de connexion en
cours que nous pouvons accepter avant de commencer à rejeter les
clients arrivants. Pensez-y comme à une file d'attente pour vos
appels téléphoniques. Le module Socket de bas niveau a un symbole
spécial pour le maximum du système, qui est SOMAXCONN.
Reuse
est nécessaire pour que nous puissions
redémarrer notre serveur manuellement sans devoir attendre quelques
minutes que les tampons du système soient vidés.
Une fois que la socket générique du serveur a été créée en utilisant
les paramètres listés ci-dessus, le serveur attend qu'un nouveau
client s'y connecte. Le serveur se bloque dans la méthode accept
,
qui devient finalement une connexion bidirectionnelle avec le client
distant (faites un autoflush sur ce handle pour éviter toute mise en
tampon).
Pour être plus gentil avec les utilisateurs, notre serveur leur offre
un prompt pour qu'ils entrent leurs commandes. La plupart des serveurs
ne font pas cela. À cause du prompt sans caractère de nouvelle ligne,
vous devrez utiliser la variante sysread
du client interactif
ci-dessus.
Ce serveur accepte cinq commandes différentes, renvoyant la sortie au client. Notez que contrairement à la plupart des serveurs en réseau, celui-ci ne sait gérer qu'un seul client à la fois. Les serveurs multithreads sont traités dans le chapitre 6 du Chameau (``Programmation en Perl'', NDT, avec un dromadaire sur la couverture).
Voici le code.
#!/usr/bin/perl -w use IO::Socket; use Net::hostent; # pour la version OO de gethostbyaddr
$PORT = 9000; # choisir quelque chose qui n'est pas utilise
$server = IO::Socket::INET->new( Proto => 'tcp', LocalPort => $PORT, Listen => SOMAXCONN, Reuse => 1);
die "can't setup server" unless $server; print "[Server $0 accepting clients]\n";
while ($client = $server->accept()) { $client->autoflush(1); print $client "Welcome to $0; type help for command list.\n"; $hostinfo = gethostbyaddr($client->peeraddr); printf "[Connect from %s]\n", $hostinfo->name || $client->peerhost; print $client "Command? "; while ( <$client>) { next unless /\S/; # ligne vide if (/quit|exit/i) { last; } elsif (/date|time/i) { printf $client "%s\n", scalar localtime; } elsif (/who/i ) { print $client `who 2>&1`; } elsif (/cookie/i ) { print $client `/usr/games/fortune 2>&1`; } elsif (/motd/i ) { print $client `cat /etc/motd 2>&1`; } else { print $client "Commands: quit date who cookie motd\n"; } } continue { print $client "Command? "; } close $client; }
Un autre type de configuration client-server n'utilise pas de connexions, mais des messages. Les communications UDP nécessitent moins de ressources mais sont aussi moins fiables, car elles ne promettent pas du tout que les messages vont arriver, sans parler de leur ordre ou de l'état dans lequel ils seront. Toutefois, l'UDP offre certains avantages sur TCP, à commencer par la capacité à faire des ``broadcasts'' ou des ``multicasts'' à destination de tout un tas d'hôtes d'un seul coup (habituellement sur votre sous-réseau local). Si vous êtes très inquiet sur la fiabilité et commencez à mettre en place des vérifications dans votre système de messages, alors vous devriez probablement juste utiliser TCP pour commencer.
Voici un programme UDP similaire au client TCP Internet donné
précédemment. Toutefois, au lieu de contacter un hôte à la fois, la
version UDP en contactera de nombreux de façon asynchrone en simulant
un multicast puis en utilisant select()
pour attendre une activité sur
les E/S pendant dix secondes. Pour faire quelque chose de similaire en
TCP, vous seriez obligé d'utiliser un handle de socket différent pour
chaque hôte.
#!/usr/bin/perl -w use strict; use Socket; use Sys::Hostname;
my ( $count, $hisiaddr, $hispaddr, $histime, $host, $iaddr, $paddr, $port, $proto, $rin, $rout, $rtime, $SECS_of_70_YEARS);
$SECS_of_70_YEARS = 2208988800;
$iaddr = gethostbyname(hostname()); $proto = getprotobyname('udp'); $port = getservbyname('time', 'udp'); $paddr = sockaddr_in(0, $iaddr); # 0 means let kernel pick
socket(SOCKET, PF_INET, SOCK_DGRAM, $proto) || die "socket: $!"; bind(SOCKET, $paddr) || die "bind: $!";
$| = 1; printf "%-12s %8s %s\n", "localhost", 0, scalar localtime time; $count = 0; for $host (@ARGV) { $count++; $hisiaddr = inet_aton($host) || die "unknown host"; $hispaddr = sockaddr_in($port, $hisiaddr); defined(send(SOCKET, 0, 0, $hispaddr)) || die "send $host: $!"; }
$rin = ''; vec($rin, fileno(SOCKET), 1) = 1;
# timeout after 10.0 seconds while ($count && select($rout = $rin, undef, undef, 10.0)) { $rtime = ''; ($hispaddr = recv(SOCKET, $rtime, 4, 0)) || die "recv: $!"; ($port, $hisiaddr) = sockaddr_in($hispaddr); $host = gethostbyaddr($hisiaddr, AF_INET); $histime = unpack("N", $rtime) - $SECS_of_70_YEARS ; printf "%-12s ", $host; printf "%8d %s\n", $histime - time, scalar localtime($histime); $count--; }
Même si la CIP du System V n'est pas aussi largement utilisée que les
sockets, elle a des usages intéressants. Vous ne pouvez pas,
toutefois, utiliser efficacement la CIP du Système V ou le mmap()
de
Berkeley pour obtenir de la mémoire partagée afin de partager une
variable entre plusieurs processus. C'est parce que Perl réallouerait
votre chaîne alors que vous ne le voulez pas.
Voici un petit exemple montrant l'utilisation de la mémoire partagée.
use IPC::SysV qw(IPC_PRIVATE IPC_RMID S_IRWXU);
$size = 2000; $id = shmget(IPC_PRIVATE, $size, S_IRWXU) || die "$!"; print "shm key $id\n";
$message = "Message #1"; shmwrite($id, $message, 0, 60) || die "$!"; print "wrote: '$message'\n"; shmread($id, $buff, 0, 60) || die "$!"; print "read : '$buff'\n";
# le tampon de shmread est cadre a droite par des caracteres nuls. substr($buff, index($buff, "\0")) = ''; print "un" unless $buff eq $message; print "swell\n";
print "deleting shm $key\n"; shmctl($id, IPC_RMID, 0) || die "$!";
Voici un exemple de sémaphore :
use IPC::SysV qw(IPC_CREAT);
$IPC_KEY = 1234; $id = semget($IPC_KEY, 10, 0666 | IPC_CREAT ) || die "$!"; print "shm key $id\n";
Mettez ce code dans un fichier séparé pour être exécuté dans plus d'un processus. Appelez le fichier take :
# creation d'un semaphore
$IPC_KEY = 1234; $id = semget($IPC_KEY, 0 , 0 ); die if !defined($id);
$semnum = 0; $semflag = 0;
# semaphore 'take' # on attend que le semaphore soit à zero $semop = 0; $opstring1 = pack("s!s!s!", $semnum, $semop, $semflag);
# on incremente le compteur du semaphore $semop = 1; $opstring2 = pack("s!s!s!", $semnum, $semop, $semflag); $opstring = $opstring1 . $opstring2;
semop($id,$opstring) || die "$!";
Mettez ce code dans un fichier séparé pour être exécuté dans plus d'un processus. Appelez ce fichier give :
# 'give' le semaphore # faites tourner ceci dans le processus originel, et vous verrez # que le second processus continue
$IPC_KEY = 1234; $id = semget($IPC_KEY, 0, 0); die if !defined($id);
$semnum = 0; $semflag = 0;
# Decremente le compteur du semaphore $semop = -1; $opstring = pack("s!s!s!", $semnum, $semop, $semflag);
semop($id,$opstring) || die "$!";
Le code de CIP SysV ci-dessus fut écrit il y a longtemps, et il est définitivement déglingué. Pour un look plus moderne, voir le module IPC::SysV qui est inclus dans Perl à partir de la version 5.005.
Un petit exemple démontrant les files de messages SysV :
use IPC::SysV qw(IPC_PRIVATE IPC_RMID IPC_CREAT S_IRWXU);
my $id = msgget(IPC_PRIVATE, IPC_CREAT | S_IRWXU);
my $sent = "message"; my $type = 1234; my $rcvd; my $type_rcvd;
if (defined $id) { if (msgsnd($id, pack("l! a*", $type_sent, $sent), 0)) { if (msgrcv($id, $rcvd, 60, 0, 0)) { ($type_rcvd, $rcvd) = unpack("l! a*", $rcvd); if ($rcvd eq $sent) { print "okay\n"; } else { print "not okay\n"; } } else { die "# msgrcv failed\n"; } } else { die "# msgsnd failed\n"; } msgctl($id, IPC_RMID, 0) || die "# msgctl failed: $!\n"; } else { die "# msgget failed\n"; }
La plupart de ces routines retournent silencieusement mais poliment
undef
lorsqu'elles échouent, au lieu de mourir sur-le-champ à cause
d'une exception non piégée (en vérité, certaines des nouvelles
fonctions de conversion Socket font un croak()
sur les arguments
mal définis). Il est par conséquent essentiel de vérifier les valeurs
de retour de ces fonctions. Débutez toujours vos programmes utilisant
des sockets de cette façon pour obtenir un succès optimal, et
n'oubliez pas d'ajouter dans les serveurs l'option -T de
vérification de pollution dans la ligne #! :
#!/usr/bin/perl -Tw use strict; use sigtrap; use Socket;
Toutes ces routines créent des problèmes de portabilité spécifiques à chaque système. Comme il est noté par ailleurs, Perl est à la merci de vos bibliothèques C pour la plus grande partie de comportement au niveau système. Il est probablement plus sûr de considérer comme déficiente la sémantique des signaux de SysV et de s'en tenir à de simples opérations TCP et UDP sur les sockets ; e.g., n'essayez pas de passer des descripteurs de fichiers ouverts par une socket datagramme UDP locale si vous voulez que votre code ait une chance d'être portable.
Comme il l'a été mentionné dans la section sur les signaux, puisque
peu de fournisseurs offrent des bibliothèques C réentrantes de façon
sûre, le programmeur prudent fera peut de choses dans un handler à
part modifier la valeur d'une variable numérique préexistante ; ou,
s'il est bloqué dans un appel système lent (redémarrage), il utilisera
die()
pour lever une exception et sortir par un longjmp(3). En fait,
même ceci peut dans certains cas provoquer un coredump. Il est
probablement meilleur d'éviter les signaux sauf lorsqu'ils sont
absolument inévitables. Ce problème sera réglé dans une future version
de Perl.
La programmation d'applications en réseau est bien plus vaste que ceci, mais cela devrait vous permettre de débuter.
Pour les programmeurs intrépides, le livre indispensable est Unix Network Programming par W. Richard Stevens (publié chez Addison-Wesley). Notez que la plupart des livres sur le réseau s'adressent au programmeur C ; la traduction en Perl est laissée en exercice pour le lecteur.
La page de manuel IO::Socket(3) décrit la bibliothèque objet, et la
page Socket(3)
l'interface de bas niveau vers les sockets. Au-delà des
fonctions évidentes dans la page de manuel perlfunc, vous devriez aussi jeter un oeil
sur le fichier modules de votre miroir CPAN le plus proche (Voir
la page de manuel perlmodlib ou mieux encore, la Perl FAQ, pour une description de
ce qu'est le CPAN et pour savoir où le trouver).
La section 5 du fichier modules est consacrée au ``Networking, Device Control (modems), and Interprocess Communication'', et contient de nombreux modules en vrac, de nombreux modules concernant le réseau, les opérations de Chat et d'Expect, la programmation CGI, le DCE, le FTP, l'IPC, NNTP, les Proxy, Ptty, les RPC, le SNMP, le SMTP, Telnet, les Threads, et ToolTalk - pour n'en citer que quelques-uns.
Tom Christiansen, avec des vestiges occasionnels de la version originale de Larry Wall et des suggestions des porteurs de Perl.
Cette traduction française correspond à la version anglaise distribuée avec perl 5.6.0. Pour en savoir plus concernant ces traductions, consultez http://perl.enstimac.fr/.
Roland Trique <roland.trique@free.fr>
Conseils : Guy Decoux <decoux@moulon.inra.fr>, Guillaume van der Rest <vanderrest@magnet.fsu.edu>
Guy Decoux <decoux@moulon.inra.fr>, Guillaume van der Rest <vanderrest@magnet.fsu.edu>