diff options
author | Vivien <vivien@pruneau.lan> | 2022-01-07 16:54:42 +0100 |
---|---|---|
committer | Vivien Kraus <vivien@planete-kraus.eu> | 2022-01-12 19:32:41 +0100 |
commit | a37d792b4840a3810c688fd254b3f745d0082c51 (patch) | |
tree | 2f19c257bec2843c2bac7596dd7e2c4b66967f75 | |
parent | bb549183b6bc1ab3a7e0124ca02323b181a3a6f2 (diff) |
Premier post
-rw-r--r-- | org/_posts/2022-01-07-démarrer-un-serveur-avant-de-connaître-son-adresse-IP.org | 283 |
1 files changed, 283 insertions, 0 deletions
diff --git a/org/_posts/2022-01-07-démarrer-un-serveur-avant-de-connaître-son-adresse-IP.org b/org/_posts/2022-01-07-démarrer-un-serveur-avant-de-connaître-son-adresse-IP.org new file mode 100644 index 0000000..a14d7fe --- /dev/null +++ b/org/_posts/2022-01-07-démarrer-un-serveur-avant-de-connaître-son-adresse-IP.org @@ -0,0 +1,283 @@ +#+options: toc:nil + +#+begin_export html +--- +layout: default +title: Démarrer un serveur avant de connaître son adresse IP +excerpt: Lorsque vous démarrez votre serveur sur un réseau dynamique, il est possible qu’il démarre avant que l’adresse IP soit connue. Que faire ? +--- +#+end_export + +C’est un problème qui impacte tous les bidouilleurs à domicile. C’est +déjà suffisamment complexe de configurer sa Bidule Box, mais un +nouveau problème intervient. À la maison, vous avez sans doute une +adresse IP dynamique. C’est-à-dire que si vous débranchez votre +routeur et que vous le rebranchez, vous obtiendrez une nouvelle +adresse IP. Sur ma connexion Escrorange, toutefois, il semble que ce +ne soit le cas que pour IPv4. Malheureusement, cela ne change rien au +problème finalement : lorsque mon ordi démarre, il y a un court laps +de temps pendant lequel le réseau est initialisé, mais je n’ai pas +encore d’adresse IP. Ou alors, je ne les ai pas encore toutes. + +Si mon service réseau décide de démarrer à ce moment-là, je me +retrouve bien embarassé : il ne sera tout simplement pas accessible +(ou seulement partiellement), et le problème ne sera peut-être jamais +détecté. + +La solution retenue par systemd [[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/configuring_and_managing_networking/systemd-network-targets-and-services_configuring-and-managing-networking][consiste à définir une cible +=network-online.target=]], qui ne sera activée que quand la négociation +des adresses sera terminée. Comme noté sur le site de Red Hat, cette +solution n’est utile que pour le démarrage du système. En cas de panne +temporaire du réseau sans redémarrage de la machine, il n’y aura pas +de redémarrage du service. Malgré cette limitation, la solution +s’applique à tous les services nécessitant de se lier au réseau. Mais +peut-être est-il possible de concevoir mieux chaque service, afin que +le problème ne se pose pas ? + +Nous allons aujourd’hui voir comment il est possible de concevoir le +service réseau de sorte à ce qu’il puisse réagir à la présence d’une +nouvelle adresse IP à lier, ou à la disparition d’une adresse +utilisée. + +* Dites bonjour :) +Nous allons concevoir un serveur très simple : il lui est donné comme +configuration un nom de domaine et un nom de service, et il se liera à +toutes les adresses connues de ce domaine, selon le protocole TCP, +dont le port correspond au service. Lorsqu’un client se connectera, on +enverra "Bonjour :)" et on terminera la connexion. + +Comme on s’attend à ce qu’il y ait plusieurs adresses IP à lier, il y +aura donc plusieurs sockets à surveiller en même temps. Il y a de +bonnes API pour faire cela, comme par exemple [[https://en.wikipedia.org/wiki/Epoll][epoll]], mais nous allons +nous contenter de la fonction =select= car elle est disponible +directement en guile. + +#+name: classe-serveur +#+caption: classe-serveur : définition de la classe serveur +#+begin_src scheme :eval no + (define-class <serveur> () + (sockets #:init-value '()) + (hôte #:init-keyword #:hôte #:init-thunk gethostname) + (service #:init-keyword #:service #:init-value "12345")) +#+end_src + +Le nom de l’hôte et le nom du service sont des constantes pour cette +classe. En effet, il serait trop complexe de gérer plusieurs noms +d’hôtes différents. La valeur par défaut du nom d’hôte est le nom de +l’ordinateur (que l’on considère comme dynamique, même s’il serait +assez incongru qu’il change sans que les adresses IP associées +suivent). La valeur par défaut du service, en revanche, est +arbitraire, et c’est au développeur de la fixer. + +* Relier le serveur en cas de besoin +Nous pouvons définir une méthode, =relier=, qui s’occupera de +récupérer la liste des adresses IP pour l’hôte, afin de fermer les +sockets obsolètes et d’en lier de nouvelles pour les nouvelles +adresses. Il ne faudrait pas fermer les sockets qui sont toujours +valides, autrement les connexions en attente seraient rejetées. + +Par choix, nous allons éviter de faire trop de mutations dans notre +API. La fonction retournera donc deux valeurs : un nouveau serveur, et +la liste des sockets obsolètes. Charge à l’utilisateur de l’API de les +fermer. + +#+name: fonction-relier +#+caption: fonction-relier : mettre à jour la liste des sockets à lier +#+begin_src scheme :eval no + (define-method (relier (serveur <serveur>)) + (define (index adresse) + ;; retourne une valeur d’index pour adresse, comparable avec + ;; equal? + (list + (sockaddr:fam adresse) + (sockaddr:path adresse) + (sockaddr:addr adresse) + (sockaddr:port adresse))) + (define tcp + (protoent:proto (getprotobyname "tcp"))) + (let ((sockets-existantes + (make-hash-table)) + (nouveau-serveur (shallow-clone serveur))) + (for-each + (lambda (socket) + (let ((adresse (getsockname socket))) + (hash-set! sockets-existantes + ;; On ne peut pas utiliser les adresses telles + ;; quelles, sinon equal? ne fonctionnera pas. + (index adresse) + socket))) + (slot-ref serveur 'sockets)) + (slot-set! + nouveau-serveur 'sockets + (map + (lambda (info-adresse) + (let ((adresse (addrinfo:addr info-adresse))) + (let ((existante + (hash-ref sockets-existantes + (index adresse)))) + (if existante + (begin + (hash-remove! sockets-existantes (index adresse)) + existante) + ;; Nouvelle adresse + (let ((s (socket (addrinfo:fam info-adresse) + (addrinfo:socktype info-adresse) + (addrinfo:protocol info-adresse)))) + (bind s adresse) + (listen s 10) + s))))) + (getaddrinfo (slot-ref serveur 'hôte) + (slot-ref serveur 'service) + (logior AI_PASSIVE) + 0 ;; Famille + SOCK_STREAM ;; Avec connexion + tcp))) + (values nouveau-serveur + (map cdr (hash-map->list cons sockets-existantes))))) + +#+end_src + +La fonction construit une table de hachage qui retient la liste des +sockets, indexée par l’adresse de la socket. Puisque les adresses IP +sont converties en nombre par guile, la fonction de comparaison +=equal?= permet de retrouver les adresses nécessaires pour satisfaire +=getaddrinfo=. + +Par souci de facilité, nous allons appeler directement la fonction +=relier= dans la méthode d’initialisation de la classe de serveur. + +#+name: initialisation-serveur +#+caption: initialisation-serveur : initialisation de la classe <serveur> +#+begin_src scheme :eval no + (define-method (initialize (object <serveur>) initargs) + (next-method) + (receive (serveur _) + (relier object) + (slot-set! object 'sockets + (slot-ref serveur 'sockets)))) +#+end_src + +On commence bien sûr par appeler =(next-method)=, de sorte à définir +les champs hôte et service grâce à la méthode d’initialisation par +défaut, qui ne se fonde que sur la définition des slots. Il est +possible de détourner cette garantie en utilisant de l’héritage +multiple, mais nous supposerons que l’initialisation pour la classe +fille d’un héritage multiple ne fait pas directement appel à +=(next-method)=, ce qui fera disparaître le problème. + +* Utilisons =select= + +Comme nous allons utiliser la fonction select, nous devons être en +mesure d’obtenir la liste des ports ou descripteurs de fichiers à +surveiller. On peut simplement retourner la valeur du champ sockets +ici. + +#+name: fonction-ports +#+caption: fonction-ports : retourne la liste des ports à surveiller +#+begin_src scheme :eval no + (define-method (ports (serveur <serveur>)) + (values + ;; Liste des ports dont on doit surveiller la lecture + (slot-ref serveur 'sockets) + ;; Liste des ports dont on attend l’écriture + '() + ;; Liste des ports dont on attend une erreur + '())) +#+end_src + +Enfin, lorsque la fonction select aura indiqué un port disponible en +lecture, nous devons agir. En l’occurence, il faut accepter le nouveau +client, lui envoyer le bonjour, et fermer la socket du client. En +revanche, si la socket ne fait pas partie du serveur, il ne faut rien +faire. + +#+name: fonction-prête +#+caption: fonction-prête : agit lorsqu’un port est prêt +#+begin_src scheme :eval no + (define-method (prête (serveur <serveur>) socket direction) + (when (port? socket) + (set! socket (port->fdes socket))) + (for-each + (lambda (socket-serveur) + (when (and (eq? direction 'lire) + (eqv? (port->fdes socket-serveur) socket)) + (let ((client (accept socket-serveur))) + (let ((port (car client)) + (adresse (cdr client))) + (format port "Bonjour ~a :)\n" + (inet-ntop (sockaddr:fam adresse) + (sockaddr:addr adresse))) + (close-port port))))) + (slot-ref serveur 'sockets)) + serveur) +#+end_src + +* Le programme à exécuter +Nous pouvons maintenant exécuter [[file:../../../code/bonjour.scm]] : + +#+begin_src scheme :eval no :tangle ../bonjour.scm :noweb no-export #:shebang "#!/usr/local/bin/guile" + (use-modules (oop goops) (ice-9 receive) (ice-9 match) + (srfi srfi-19)) + + <<classe-serveur>> + <<fonction-relier>> + <<initialisation-serveur>> + <<fonction-ports>> + <<fonction-prête>> + + (define (main serveur) + (format #t "~a : le serveur lie les adresses suivantes : ~a\n" + (date->string (current-date)) + (map + (lambda (socket) + (let ((adresse (getsockname socket))) + (let ((adresse + (inet-ntop (sockaddr:fam adresse) (sockaddr:addr adresse))) + (port + (sockaddr:port adresse))) + (format #f "[~a]:~a" adresse port)))) + (slot-ref serveur 'sockets))) + (receive (read write except) (ports serveur) + (match (select read write except 60) + ((read write except) + (let traiter ((serveur serveur) + (tâches + (append + (map (lambda (socket) + `(,socket lire)) + read) + (map (lambda (socket) + `(,socket écrire)) + write) + (map (lambda (socket) + `(,socket exception)) + except)))) + (match tâches + (() + (receive (serveur à-fermer) (relier serveur) + (for-each close-port à-fermer) + (main serveur))) + (((socket direction) tâches-restantes ...) + (traiter (prête serveur socket direction) + tâches-restantes)))))))) + + (main (make <serveur>)) +#+end_src + +Ce programme vérifie toutes les 60 secondes (dans le pire des cas) que +les adresses à lier sont à jour, et sert tous les clients qui se +présentent en leur répondant « Bonjour :) ». + +Il y a beaucoup de choses à améliorer. Pour commencer, il faudrait +utiliser autre chose que =select=, qui a une complexité temporelle +linéaire et qui ne gère qu’un nombre limité de clients. Nous avons +aussi une complexité linéaire pour vérifier si la socket est rattachée +au serveur, et donc quadratique pour un appel récursif de =main=. Il +faudrait utiliser =epoll=, ou une bibliothèque de gestion du réseau +plus haut niveau comme la GLib, et revoir notre code. Il faudrait +également utiliser getopt-long (en internationalisant les noms longs +d’options) pour choisir le nom d’hôte à lier. Enfin, exécuter +=getaddrinfo= à chaque fois que l’on sert un groupe de clients n’est +pas très pertinent, et attendre 60 secondes peut être long. Pour +détecter les situations où nous devrions relier le serveur, il va +falloir utiliser l’API Netlink, ce que nous ferons une prochaine fois. |