Outils pour utilisateurs

Outils du site


nsi:premiere:web:serveur_web_python

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 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 :

  • 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 :

  • si on demande localhost/ ou localhost il renvoie la page index.html,
  • si on demande localhost/nom_fichier.ext il renvoie, si il existe, le fichier nom_fichier.ext
  • si on demande localhost/nom_fichier.html mê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.html avec 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 renvoie True si la requête est de type GET, False sinon.
  • 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 html existant, 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 renvoie True si 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 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 :

  • 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).
nsi/premiere/web/serveur_web_python.txt · Dernière modification : de goupillwiki