Table des matières
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.
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()