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.

Passage à un client web

Abandonnons le client python. Maintenant on souhaite se connecter au serveur avec un client web comme Firefox ou Chrome.

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:13451 côté client.
  • On peut changer la configuration du serveur :
    1. arrêter le serveur : il est configuré pour s'arrêter quand il y a "Fin" dans la requête, alors demandez l'url http://localhost:13451/Fin
    2. modifiez le port dans serveur.py et mettez 80
    3. relancez le serveur
    4. maintenant vous pouvez demander l'url http://localhost dans 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 :

Étape 1 : analyser la requête

Nous allons créer un module requetes.py qui contiendra les fonction que nous voulons développer.

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 :

Étape 3

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.

É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

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 :

Quand c'est fonction seront valide, vous pourrez modifier le script serveur pour que :