summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVivien <vivien@pruneau.lan>2022-01-07 16:54:42 +0100
committerVivien Kraus <vivien@planete-kraus.eu>2022-01-12 19:32:41 +0100
commita37d792b4840a3810c688fd254b3f745d0082c51 (patch)
tree2f19c257bec2843c2bac7596dd7e2c4b66967f75
parentbb549183b6bc1ab3a7e0124ca02323b181a3a6f2 (diff)
Premier post
-rw-r--r--org/_posts/2022-01-07-démarrer-un-serveur-avant-de-connaître-son-adresse-IP.org283
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.