Table des matières
Un serveur web avec Python
Nous allons étudier un exemple le plus simple possible d'un serveur web s'exécutant avec Python.
socket
Pour cela nous utiliserons la bibliothèque socket de Python.
Un socket est une prise, ce qui se rapproche donc d'un port. En gros le port est identifiant d'un point d'accès, le socket identifie plutôt une connexion. Si S est le serveur et A et B des clients, A et B aboutisse sur le même port de S mais les communications S-A et S-B reçoivent un numéro différent. Ce dernier numéro est le socket.
Client et serveur en Python
Voici le script serveur.py :
# script serveur
# serveur.py
# import bibli + constantes de configuration
from socket import socket, AF_INET, SOCK_STREAM
# choix du numéro de port (max = 65535)
# certains ports comme 80 ont des usages spécifiques
port = 13451
s = socket(AF_INET, SOCK_STREAM)
s.bind(("", port)) # le script réserve le port
s.listen(5)
# socket se met à l'écoute.
# les demandes de connexions se mettent à la queue avant d'être traitées
# on en accepte jusque 5 ici, au-delà elles sont rejetées
# serveur se met en écoute -> boucle infinie
while True:
# s.accept() attend une demande de connexion
# quand la demande arrive, s.accept() renvoie
# connexion et address
connexion, address = s.accept()
print("Connexion acceptée de ", address)
requete_bin = connexion.recv(1024)
# le message venant du client peut-être plus ou moins long
# on supposera que ce message fait toujours moins de 1024 octets.
requete_str = requete_bin.decode()
# la requête est en binaire. decode: binaire -> string
if requete_str != "":
# si la requête n'est pas vide
print(requete_str) # affiche la requête
# envoi de la réponse : "Reçu"
answer_str = "Reçu"
answer_bin = answer_str.encode("utf-8")
connexion.send(answer_bin)
# fermeture de la requête
connexion.close()
# pour éteindre le serveur, il faudra que le client demande "Fin"
# extinction dès que "Fin" est contenu quelque part dans la requête
if "Fin" in requete_str:
break
s.close()
Et le script client.py
# script client
# client.py
from socket import socket, AF_INET, SOCK_STREAM
hote = "localhost" # adresse du serveur
# localhost désigne la machine elle-même.
# on pourrait aussi écrire "127.0.0.1" qui est une adresse spéciale
# désignant localhost
port = 13451
# port sur lequel écoute le serveur
# doit être cohérent avec celui du script serveur
message = input("Choisissez un message : ")
if message != "":
# création d'une connexion
s = socket(AF_INET, SOCK_STREAM)
s.connect((hote, port)) # connexion au serveur
# le message est encodé en binaire et envoyé
message_bin = message.encode("utf-8")
s.send(message_bin)
# on lit l'éventuelle réponse
# en supposant qu'elle est de taille < 1024 octets
reponse_bin = s.recv(1024)
reponse_str = reponse_bin.decode()
print("Réponse : ", reponse_str)
s.close()
Chaque exécution du client permet d'envoyer un seul message.
- Lancez deux terminaux,
- exécutez chaque script dans un terminal,
- vérifiez que le message côté client est bien transmis côté serveur et que celui-ci répond,
- n'oubliez pas d'éteindre le serveur en envoyant
“Fin”depuis le client.
Passage à un client web
Abandonnons le client python. Maintenant on souhaite se connecter au serveur avec un client web comme Firefox ou Chrome.
- Lancer le serveur comme précédemment
- Lancer un navigateur, par exemple Chrome
- Dans l'url, essayez
http://localhost(on pourrait choisi127.0.0.1)
Il ne se passe rien côté serveur et le client n'obtient pas de réponse…
Le serveur est configuré pour écouter sur le port 13451 mais par défaut, le client web envoie sa requête sur le port 80. Il faut accorder les deux :
- On peut demande l'url
http://localhost:13451côté client. - On peut changer la configuration du serveur :
- arrêter le serveur : il est configuré pour s'arrêter quand il y a
"Fin"dans la requête, alors demandez l'urlhttp://localhost:13451/Fin - modifiez le port dans
serveur.pyet mettez80 - relancez le serveur
- maintenant vous pouvez demander l'url
http://localhostdans le navigateur.
Les requête côté serveur
Le serveur affiche les requêtes qu'il reçoit. Vous pouvez ainsi voir à quoi ressemble une requête provenant du navigateur. Par exemple, si on demande http://localhost/machin.html, la requête reçue par le serveur sera :
GET /machin.html HTTP/1.1 Host: localhost Connection: keep-alive sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="101", "Google Chrome";v="101" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "Windows" Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Encoding: gzip, deflate, br Accept-Language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7
Répondre à une requête
Les fichiers à ajouter dans le répertoire de serveur.py : exo_serveur.zip
Le serveur web que l'on a mis en place n'est pas satisfaisant : quoi qu'on lui demande, il répond toujours "Reçu". On voudrait qu'il ait un comportement plus normal : on voudrait que :
- si on demande
localhost/oulocalhostil renvoie la pageindex.html, - si on demande
localhost/nom_fichier.extil renvoie, si il existe, le fichiernom_fichier.ext - si on demande
localhost/nom_fichier.htmlmême chose pour le cas particulier d'un fichier html (on verra que ce ce cas est spécial) - et si on demande un fichier qui n'existe pas, il renvoie la page
404.htmlavec le code d'erreur 404.
Étape 1 : analyser la requête
Nous allons créer un module requetes.py qui contiendra les fonction que nous voulons développer.
- Notre première fonction est
is_GET(requete:str)qui renvoieTruesi la requête est de type GET,Falsesinon. - La deuxième fonction est
get_url(requete:str)qui renvoie l'adresse de la requête sans le/initial.
Par exemple, si on demande localhost, l'adresse dans la requête sera / et on veut alors que get_url renvoie "". Et si on demande localhost/truc.html, l'adresse dans la requête sera /truc.html et on veut que get_url renvoie "truc.html".
Étape 2 : charger un fichier
On veut charger un fichier, si il existe, et charger le fichier 404.html sinon.
from os import exists
# par exemple
exists("truc.html")
# renvoie True si le fichier existe, False sinon
f = open("truc.html", 'rb') # ouverture du fichier en mode binaire
content = f.read() # lit le contenu
f.close()
# content est ce que le serveur a besoin de renvoyer
Écrire une fonction get_file(filename:str) qui :
- renvoie le fichier
filename, en binaire, s'il existe, - renvoie le fichier
404.html, en binaire, sinon
Étape 3
Modifier le serveur pour que, au lieu de renvoyer toujours "Reçu",
- il vérifie si la requête est de type GET,
- si oui, il récupère le nom de fichier demandé,
- il charge ce fichier et le renvoie
Testez et vérifiez que cela fonctionne bien pour les fichiers existants : fozzie.jpg, index.html, page.html.
Étape 4
Si vous avez essayer de charger un fichier html, vous avez dû constater que l'affichage n'était pas satisfaisant. Cela vient du fait qu'un fichier html nécessite une entête.
Dans le cas d'un chargement avec succès, l'entête, en binaire, est exactement :
"HTTP/1.1 200 OK\r\nhost: le site local\r\nContent-Type: text/html\r\n\r\n".encode()
Et s'il s'agit d'une erreur 404, l'entête est exactement :
"HTTP/1.1 404 Not Found\r\nhost: le site local\r\nContent-Type: text/html\r\n\r\n".encode()
Vous devez modifier la fonction get_file de façon que
- si l'url demandée est un fichier
htmlexistant, vous devez faire précéder la réponse de l'entête du cas succès, - si l'url demandée provoque une erreur 404, il faut faire précéder la réponse de l'entête du cas erreur 404.
Faites la modification et vérifiez le bon fonctionnement.
La réponse à un POST
Vous disposez d'une page page_post.html. Ouvrez-la et essayez d'envoyer des valeurs.
Observez la forme de la requête. Voyez comme les données ont été transmises.
Il nous faut de nouvelles fonctions :
- une fonction
is_POST(requete:str)qui renvoieTruesi la requête est de type POST - une fonction
get_params(requete:str)qui récupère les paramètres contenus dans la requête - une fonction
rep_post(params)qui renvoie le contenu dereponse_post.htmlen le complétant avec la valeur des paramètres.
Quand c'est fonction seront valide, vous pourrez modifier le script serveur pour que :
- si le type de requête est POST,
- on récupère les paramètres avec
get_params, - on produit la réponse html avec
rep_post(params).
