La programmation en Perl: Les trucs du réseau (Deuxiéme partie)

par Hal Pomeranz, traduction Philippe Bereski

Dans notre dernier article, nous avons vu comment programmer un client en écrivant un simple utilitaire qui récupère les pages d'un serveur Web distant. Dans celui-ci, nous allons étudier l'écriture d'un petit serveur réseau. En guise d'application nous écrirons un serveur Web primaire. (L'intégralité du code de ce serveur se trouve à la fin du présent article pour les lecteurs qui trouveraient plus commode de le suivre au fure et à mesure des explications). N'hésitez pas à relire l'article précédant si vous n'avez plus en tête les concepts de base utilisés dans cette présentation.

Le point de départ

Avant toute chose, un serveur doit établir une sockette sur laquelle il va pouvoir recevoir et accepter des demandes de connexion. La première partie de ce process ressemble donc beaucoup à celle du client :
     use Socket;
     $this_host = `my-server.netmarket.com'; 
     $port = 8080; 
     $server_addr = (gethostbyname($this_host))[4]; 
     $server_struct = pack("S n a4 x8", AF_INET, $port, $server_addr); 
     $proto = (getprotobyname(`tcp'))[2]; 
     socket(SOCK, PF_INET, SOCK_STREAM, $proto)||  die "Failed to initialize socket: $!\n";
Dans un premier temps le programme charge le module Perl Socket.pm. Le nom de la machine où tourne ce serveur ainsi que son numéro de port sont donnés dans les 2 lignes suivantes. Ces informations peuvent très bien être définies sur la ligne de commande ou dans un fichier de configuration. Le programme invoque ensuite gethostbyname() pour obtenir l'adresse IP du serveur et créer la stucture C qui sera utilisée ultérieurement. Finalement, il invoque socket() pour créer un file handle sur cette sockette.

Souvenez vous de l'article précédant, le serveur Web utilisait le port 80 par défaut. Pourquoi donc cet exemple utilise-t-il le port 8080 ? Pour des questions de sécurité, les numéros de port inférieurs à 1024 ne peuvent être utilisés que par des serveurs lancés par root. L'arrière pensée de cette règle était de "garantir" que de tels serveurs (telnet, ftp, gopher, ... ), étaient administrés par des personnes "responsables" et que l'on pouvait donc s'y connecter en toute sécurité. Depuis l'explosion du nombre des stations de travail connectées au réseau, cette garantie n'en est plus une.

Revenons à notre exemple. Le serveur doit maintenant se préparer à recevoir des demandes de connexion sur le port et l'adresse définis :

     setsockopt(SOCK, SOL_SOCKET, SO_REUSEADDR,1) || 
          die "setsockopt() failed: $!\n"; 
     bind(SOCK, $server_struct) || die "bind() failed: $!\n"; 
     listen(SOCK, SOMAXCONN) || die "listen() failed: $!\n";
La fonction setsockopt() permet au programme de changer certains paramètres de la sockette. Nous allons revenir sur le paramètre SO_REUSEADDR dans un instant. La fonction bind() réalise la connexion entre le file handle SOCK et l'adresse et le port donnés au début du programme. Tant qu'un programme demeure lié à un couple (port, adresse), aucun autre peut le faire. Ceci est très utile et évite bien des confusions. Cependant même après que le serveur soit mort, ce couple (port, adresse) reste inutilisable aussi longtemps que la machine n'a pas rebouté, même si on réexécute exactement le même serveur. Ceci peut paraître étrange voir même être gênant. En positionnant à vrai (1) le bit SO_REUSEADDR - AVANT l'appel à bind() - on autorisera d'autres programmes à réutiliser cette adresse après la mort du serveur courant. Les codes SOL_SOCKET et SO_REUSEADDR sont des constantes définies dans Socket.pm.

La fonction listen() est certainement mal nommée. Elle ne fait rien d'autre que spécifier la taille de la file des requêtes en attente de traitement par le serveur. Pratiquement toutes les implémentations de la couche sockette limitent cette queue à 5 entrées. Vous avez donc intèrêt à les traiter rapidement. SOMAXCONN, encore une constante définie dans Socket.pm, est généralement intialisée à 5. Si vous essayez une valeur plus grande le système d'exploitation limitera de toutes façon la queue à 5 entrées. Solaris 2.x est, à ma connaissance, le seul système d'exploitation moderne qui accepte une valeur supérieure à 5. (Il est amusant de noter que SOMAXCONN reste défini à 5 dans les headers du système).

Le traitement des requêtes en attende

Arrivé ici, la plus part des serveurs réseau entrent dans une courte boucle où ils traitent rapidement les requêtes arrivant :
     for (;;) { 
          $remote_host = accept(NEWSOCK, SOCK); 
          die "accept() error: $!\n" unless ($remote_host);
 
          # do some work here
          close(NEWSOCK); 
     }
L'appel accept() extrait de la file de la sockette SOCK la requête suivante. Si il n'y a plus de requête, accept attend la prochaine. Une nouvelle sockette est crée, elle sera le nouveau point de communication entre la machine et son client distant. Les données écrites dans NEWSOCK seront envoyées à ce client, de même que les données lues dans NEWSOCK proviennent de ce client. Pensez toujours à refermer vos sockettes quand vous n'en avez plus besoin.

La fonction accept rend une structure C contenant l'adresse de la machine distante, à moins que l'appel ait échoué, dans ce cas accept() retourne undef. Cette structure correspond à celle passée à connect. Vous pouvez l'utiliser pour récupérer l'adresse de la machine distante comme ceci :

     $raw_addr = (unpack("S n a4 x8",$remote_host))[2]; 
     @octets = unpack("C4", $raw_addr); 
     $address = join(".", @octets);
Vous pouvez également récupérer le nom de la machine distante par gethostbyname() :
     $hostname = (gethostbyaddr($raw_addr,AF_INET))[0];
Ceci est utile pour tracer les processus. Notez l'utilisation une fois encore à AF_INET; gethostbyaddr() à besoin qu'on lui indique le type de l'adresse qu'on lui passe.

Un serveur Web élémentaire

Arrivé à ce point, nous avons construit le squelette que doit possèder tout serveur réseau. Nous allons pouvoir lui faire faire quelque chose d'utile.

HTTP est un protocole incroyablement simple. Les requêtes envoyées par le butineur sont des lignes de texte ASCII terminées par une ligne vide. Une fois qu'il a rencontré cette ligne vide, le serveur envoit sa réponse au client et rompt la communication. Bien que le client envoye généralement une foule d'informations avec sa requête, en pratique le serveur peut les ignorer toutes à l'exception de celles qui ont la forme suivante :

     GET /some/path/to/file.html ...
Voici un exemple de code qui extrait de la requête du client le chemin à l'information demandée :
     while (<NEWSOCK>) { 
          last if (/^\s*$/); 
          next unless (/^GET /); 
          $path = (split(/\s+/))[1]; 
     }
Le serveur se doit maintenant de répondre. En général, le chemin est relatif à la racine d'un répertoire où se trouvent les informations gérées par le serveur - la $docroot dans le jargon du Web. Elle peut-être définie dans un fichier config ou sur la ligne de commande. En supposant que $docroot a été définie d'une façon ou une autre, on peut simplement écrire :
     if (open(FILE, "< $docroot$path")) { 
          @lines = <FILE>; 
          print NEWSOCK @lines; 
          close(FILE); 
     } 
     else { 
          print NEWSOCK <<"EOErrMsg"; 
     <TITLE>Error</TITLE><H1>Error</H1> 
     The following error occurred while
     trying to retrieve your information:
     $! 
     EOErrMsg 
     }
Si le fichier requis est accessible, le serveur le recopie simplement dans le file handle NEWSOCK. Notez qu'en cas de problème sur open(), le serveur envoie un message d'erreur. Noubliez jamais que vous avez un correspondant à l'autre bout de la sockette et qu'il ou elle s'attend à recevoir une réponse à sa requête.

Bravo. Si vous rassemblez les divers éléments de code que nous venons d'examiner, vous disposez du squelette d'un serveur Web. L'intégralité du code est donné à la fin de cet article pour vous permettre de mieux revoir tous les concepts que nous venons d'explorer.

Ce n'est pas tout

Bien que ce serveur suffise à traiter des requêtes simples, il possède un certain nombre d'insuffisances. La première et la plus importante, il ne peut traiter qu'une seule requête à la fois. La plus part des serveurs de production en traitent simultanément de 100 à 1000. Enfin, si vous laissez tourner ce serveur sur votre machine, je pourrai lui envoyer la requête suivante :
  /../../../../../../../etc/passwd
et je récupèrerai votre fichier des mots de passe. Il est clair que vous aurez besoin d'un contrôle d'accès.

Dans le troisième et dernier article de cette série, nous verrons comment résoudre, entre autre, ces 2 problèmes de notre mini serveur Web.

     #!/packages/misc/bin/perl

     use Socket;

     $docroot = `/home/hal/public_html'; 
     $this_host = `my-server.netmarket.com'; 
     $port = 8080;

     # Initialize C structure 
     $server_addr =(gethostbyname($this_host))[4]; 
     $server_struct = pack("S n a4 x8", AF_INET,$port, $server_addr);

     # Set up socket 
     $proto = (getprotobyname(`tcp'))[2]; 
     socket(SOCK, PF_INET, SOCK_STREAM,$proto)|| die "Failed to initialize socket:$!\n";

     # Bind to address/port and set up pending queue 
     setsockopt(SOCK, SOL_SOCKET, SO_REUSEADDR, 1) || die "setsockopt() failed: $!\n"; 
     bind(SOCK, $server_struct) || die "bind() failed: $!\n"; 
     listen(SOCK, SOMAXCONN) || die "listen() failed: $!\n";

     # Deal with requests 
     for (;;) { 
          # Grab next pending request 
          # 
          $remote_host = accept(NEWSOCK, SOCK); 
          die "accept() error: $!\n" unless ($remote_host);
		
          # Read client request and get $path 		
          while (<NEWSOCK>) { 
               last if (/^\s*$/); 
               next unless (/^GET /); 
               $path = (split(/\s+/))[1]; 
          }

          # Print a line of logging info to STDOUT 
          $raw_addr = (unpack("S n a4 x8", $remote_host))[2];
          $dot_addr = join(".", unpack("C4", $raw_addr)); 
          $name = (gethostbyaddr($raw_addr, AF_INET))[0]; 
          print "$dot_addr\t$name\t$path\n";
		
          # Respond with info or error message 
          if (open(FILE, "< $docroot$path")) { 
               @lines = <FILE>; 
               print NEWSOCK @lines; 
               close(FILE); 
          } 
          else { 
               print NEWSOCK <<"EOErrMsg"; 
     <TITLE>Error</TITLE><H1>Error</H1> 
     The following error occurred while trying to retrieve your information: $! 
     EOErrMsg 
          }

          # All done 
          close(NEWSOCK); 
     }
Traduction de ;login: Vol. 21 No. 5, Octobre 1996.


Dernière édition: 15 mars 1998 phb
Original 12/5/96
Back to the original
Retour à l'index