Nous allons étudier un exemple le plus simple possible d'un serveur web s'exécutant avec Python.
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.
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.
“Fin” depuis le client.Abandonnons le client python. Maintenant on souhaite se connecter au serveur avec un client web comme Firefox ou Chrome.
http://localhost (on pourrait choisi 127.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 :
http://localhost:13451 côté client."Fin" dans la requête, alors demandez l'url http://localhost:13451/Finserveur.py et mettez 80http://localhost dans le navigateur.
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
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 :
localhost/ ou localhost il renvoie la page index.html,localhost/nom_fichier.ext il renvoie, si il existe, le fichier nom_fichier.extlocalhost/nom_fichier.html même chose pour le cas particulier d'un fichier html (on verra que ce ce cas est spécial)404.html avec le code d'erreur 404.
Nous allons créer un module requetes.py qui contiendra les fonction que nous voulons développer.
is_GET(requete:str) qui renvoie True si la requête est de type GET, False sinon.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".
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 :
filename, en binaire, s'il existe,404.html, en binaire, sinon
Modifier le serveur pour que, au lieu de renvoyer toujours "Reçu",
Testez et vérifiez que cela fonctionne bien pour les fichiers existants : fozzie.jpg, index.html, page.html.
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
html existant, vous devez faire précéder la réponse de l'entête du cas succès,Faites la modification et vérifiez le bon fonctionnement.
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 :
is_POST(requete:str) qui renvoie True si la requête est de type POSTget_params(requete:str) qui récupère les paramètres contenus dans la requêterep_post(params) qui renvoie le contenu de reponse_post.html en le complétant avec la valeur des paramètres.Quand c'est fonction seront valide, vous pourrez modifier le script serveur pour que :
get_params,rep_post(params).