TP- 5 - Serveur en Python, pour aller plus loin

by Joseph Razik, on 2022-03-09

Pour pouvoir traiter plusieurs clients en parallèle, il existe deux grandes familles de solutions :

  • La première, la plus classique, est qu'un processus serveur écoute les connexions et à chaque demande créée un autre processus pour assurer le traitement en lui passant la socket.
  • La seconde utilise une technique de pooling, c'est-à-dire que le serveur va regarder périodiquement dans toutes les connexions dont il dispose celles où il peut lire ou écrire.

1   Un serveur multitâche avec plusieurs fils

L'exemple suivant montre comment on peut créer un serveur multitâche en Python 3.

Étudiez l'exemple suivant et le tester avec la commande telnet.

# coding: utf-8

import socket
import sys
import threading

# Le serveur se met en écoute

HOST = '0.0.0.0'
PORT = 2004

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Bind socket to local host and port
try:
    s.bind((HOST, PORT))
except socket.error as msg:
    print("Bind failed. Error Code : " + str(msg.errno) + " Message " + msg.strerror)
    sys.exit()

s.listen(10)

print("Serveur en écoute sur " + HOST + ":" + str(PORT))

# Cette fonction sera utilisée dans les threads qui traiteront les connexions
def clientthread(conn):
    conn.send(("Hello from " + HOST + ":" + str(PORT) + "\n").encode('UTF-8'))

    # Mettre ici l'automate du serveur.
    # Ici une boucle infinie arrêtée lors de l'envoi d'un chaîne vide
    while True:
        # Receiving from client
        data = conn.recv(1024).decode('UTF-8')
        reply = 'Recu: ' + data

        # si la reponse est FIN ou un ligne vide, le dialogue d'arrête.
        if data.upper().strip() == "FIN" or data.strip() == "":
            break

        conn.sendall(reply.encode('UTF-8'))
    conn.close()

# La boucle d'attente des connexions
while True:
    try:
        conn, addr = s.accept()
        print('Connexion de ' + addr[0] + ':' + str(addr[1]))

        # Création de la thread qui prendra en charge la connexion.
        # threading.Thread( group=None, target=None, name=None, args=(),
        # kwargs={}) où :
        #    group doit rester à None, en attendant que la classe
        #        ThreadGroup soit implantée.
        #    target est la fonction appelée par le Thread.
        #    name est le nom du Thread.
        #    args est un tuple d'arguments pour l'invocation de la fonction
        #        target
        #    kwargs est un dictionnaire d'argumens pour l'invocation de la
        #        fonction target

        t = threading.Thread(None, clientthread, None, (conn,), {})
        t.start()

    except KeyboardInterrupt:
        print("Stop.\n")
        break

s.close()

En vous inspirant de cet exemple, écrire un serveur qui comprend les commandes suivantes :

  • DATA (le serveur attend ensuite un liste d'entier sous la forme de chaîne de caractères sur une ligne, une ligne vide indique la fin des données). Plusieurs commandes DATA peuvent être envoyées. - SUM, MIN, MAX, ... qui retourne le résultat de la fonction sur la liste des dernières données saisies. Il doit donc y avoir eu au moins un DATA.
  • HEAD X : retourne les entiers avant la position X
  • TAIL Y : retourne les entiers après la position X
  • INTERVAL X Y : retourne les entiers entre les positions X et Y
  • FIN termine la session

Voilà un exemple d'échange :

DATA
12
3
5

MIN
>3
MAX
>12
HEAD 2
>12 3
TAIL 1
> 5

2   Un serveur qui écoute plusieurs clients

La solution à base de pooling utilise la méthode suivante :

read_sockets, write_sockets, error_sockets = select.select(CONNECTION_LIST, [], [])

Étudiez l'exemple suivante et le tester avec telnet.

# coding: utf-8

""" Tcp Chat server. """

import socket
import select

CONNECTION_LIST = []
RECV_BUFFER = 4096
HOST = "0.0.0.0"
PORT = 5000

def broadcastToClients(the_sock, message):
    # Do not send the message to master socket and the client who has send us
    # the message
    for sock in CONNECTION_LIST:
        if sock != server_socket and socket != the_sock:
            try:
                print(message)
                sock.send(message.encode('UTF-8'))
            except:
                # La ligne suivante permet d'afficher l'erreur
                # print("Unexpected error: "+sys.exc_info()[0])
                # En général, c'est le client qui n'est disponible
                # On ferme la connexion et on le supprime
                sock.close()
                CONNECTION_LIST.remove(sock)

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((HOST, PORT))
server_socket.listen(10)

# On ajoute le serveur à la liste des connexions pour pouvoir "lire"
# la connexion d'un nouveau client

CONNECTION_LIST.append(server_socket)

print("Chat server started " + HOST + ":" + str(PORT))

while 1:
    try:
        # On recupère la liste des sockets dans lesquelles on peut lire dans
        # read_sockets
        # écrire dans write_sockets...
        read_sockets, write_sockets, error_sockets = select.select(
            CONNECTION_LIST, [], [])

        for sock in read_sockets:
            # Si on peut lire dans celle du serveur c'est une nouvelle
            # connexion
            if sock == server_socket:
                # On l'accepte et on ajoute la connexion à CONNECTION_LIST
                sockfd, addr = server_socket.accept()
                CONNECTION_LIST.append(sockfd)
                print("Client (" + addr[0] + ", " + str(addr[1]) +
                      ") connected")
                # On envoie le message à tous les clients
                broadcastToClients(sockfd, "[" + addr[0] + ":" + str(addr[1]) +
                                            "] entered room\n")

            # Sinon c'est qu'un message à été reçu d'un client.
            else:
                try:
                    data = sock.recv(RECV_BUFFER).decode('UTF-8')
                    if data:
                        # Le message est envoyer à tous les clients.
                        # Le premiere paramètre est la socket dans laquelle le
                        # message a été reçu pour ne pas lui retourner
                        broadcastToClients(sock, '<' +
                                           str(sock.getpeername()) + '> ' +
                                           data + '\n')

                except:
                    # La ligne suivante est utile pour savoir quelle erreur a
                    # été émise
                    # print("Unexpected error: " + sys.exc_info()[0])
                    broadcastToClients(sock, "Client [" + addr[0] + ":" +
                                       str(addr[1]) + "] is offline")
                    print("Client [" + addr[0] + ":" + str(addr[1]) +
                          "] is offline")
                    sock.close()
                    CONNECTION_LIST.remove(sock)
                    continue
    except KeyboardInterrupt:
        print("Stop.\n")
        break

server_socket.close()

Mettez à jour l'exemple précédent pour disposer d'un serveur de chat réaliste qui dispose des commandes :

  • NICKNAME qui permet de disposer d'un nom d'utilisateur
  • MSG nickname qui envoie un message au client d'un utilisateur dont le nickname est donné.

3   Applications

En utilisant l'un des modèles ci dessus, programmez les problèmes suivant dans l'ordre de votre choix.

3.1   Serveur Calculette

Écrivez un serveur qui permet à un client de gérer une pile d'entiers. Quand le serveur reçoit un entier il l'empile, quand le serveur reçoit l'une des commandes ADD, MUL, DIV, MOD il effectue le calcul avec les deux derniers entiers sur la pile et empile le résultat. Quand le serveur reçoit la commande LIST il transmet la pile sans la modifier, quand il reçoit la commande POP il dépile le dernier entier et le transmet au client. Voilà un exemple d'échange :

3
2
5
LIST
> 5 2 3
ADD
LIST
> 7 3
POP
> 7
LIST
> 3

3.2   Calcul distribué

Écrivez un serveur qui attend que des clients de travail se connectent. Quand un client (de commade) envoie la commande GO. Le serveur partage entre ses n clients de travail le calcul de la somme des racines carrées des nombres entre 1 et 100*n et envoie le résultat au client de commande.

3.3   Tri distribué

Écrivez un serveur qui attend que des clients se connectent. Quand un des clients envoie la commande GO, le serveur génère un grand tableau d'entier aléatoires, le découpe en autant de parties que de clients, puis leur envoie chacun sa partie. Les clients réalisent un tri des données réceptionnées et les renvoient au serveur. Le serveur fusionne ensuite les résultats pour afficher le résultat final.

3.4   Bataille navale

Écrivez un programme de bataille navale en réseau. Le programme sera tour à tour client puis serveur. Le programme prendra trois paramètres : le numéro du joueur (1 ou 2), n le nombre de bateaux et le port du serveur. Les bateaux sont tous de taille 1 et la grille est de taille n*n. Le fonctionnement est le suivant :

  • Les deux joueurs remplissent leur grille (des tableaux de caractères) avec .. Les n bateaux sont disposés aléatoirement (attention, à ne pas mettre deux bateaux au même endroit). Chaque joueur affiche sa grille.
  • Le joueur 1 est le serveur, le joueur 2 est le client.
  • Le joueur 1 écoute le tir du joueur 2 (deux entiers) et répond avec le nombre de bateaux restants (1 entier).
  • Les rôles sont inversés, jusqu'à ce que le nombre de bateaux restants de l'un des joueurs soit 0.
  • Chaque joueurs a donc successivement la fonction écouter et parler.

4   Un serveur « simplifié »

https://docs.python.org/3/library/socketserver.html

# coding: utf-8

import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):
       """
       The RequestHandler class for our server.

       It is instantiated once per connexion to the server, and must
       override the handle() method to implement communication to the
       client.
       """

       def handle(self):
           # self.request is the TCP socket connected to the client
           self.data = self.request.recv(1024).strip()
           print("{} wrote:".format(self.client_address[0]))
           print(self.data)
           # just send back the same data, but upper-cased
           self.request.sendall(self.data.upper())

if __name__ == "__main__":

    HOST, PORT = "localhost", 9999

    # Create the server, binding to localhost on port 9999
    server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)

    # Activate the server; this will keep running until you
    # interrupt the program with Ctrl-C
    server.serve_forever()