====== Python – les sockets ====== ===== Rappel sur TCP ===== **TCP (Transmission Control Protocol)**, est un protocole **orienté connexion**. Les applications sont connectées pour communiquer et TCP permet de s'assurer qu'une information envoyée au travers du réseau bien été réceptionnée par l'autre application. Si la connexion est rompue pour une raison quelconque, les applications doivent rétablir la connexion pour communiquer de nouveau. ===== Client et serveur ===== Il s'agit de créer deux applications : * l'application serveur qui écoute sur le réseau en attendant des connexions * l'application client qui se connecte au serveur. ==== Les différentes étapes ==== Voici dans un ordre simplifié les étapes du client et du serveur. Plus loin dans ce document vous programmerez un serveur pouvant communiquer avec plusieurs clients. **Le serveur :** * 1. attend une connexion de la part du client ; * 2. accepte la connexion quand le client se connecte ; * 3. échange des informations avec le client ; * 4. ferme la connexion. **Le client :** * 1. se connecte au serveur ; * 2. échange des informations avec le serveur ; * 3. ferme la connexion. ==== Établir une connexion ==== Pour que le client se connecte au serveur, il lui faut deux informations : * Le **nom d'hôte** (hostname) ou son adresse IP. * Un **numéro de port**, qui est souvent propre au type de service utilisé. Le numéro de port est compris entre 0 et 65535 et les numéros entre 0 et 1023 sont réservés par le système. Il est préférable de ne pas les utiliser. ==== Les sockets ==== Les **sockets** sont des objets qui permettent d'ouvrir une connexion avec une machine locale ou distante et d'échanger avec elle. Ces objets sont définis dans le module **socket** . ===== Les sockets ===== * Lancez l'interpréteur Python (Démarrer\Python 3.4\IDLE) * Importez le module socket. import socket ===== Créez le serveur ===== Il faut faire appel au constructeur socket. Dans le cas d'une connexion TCP, il prend les deux paramètres suivants, dans l'ordre : * **socket.AF_INET** : la famille d'adresses, ici ce sont des adresses Internet ; * **socket.SOCK_STREAM** : le type du socket, SOCK_STREAM pour le protocole TCP. >>> connexion_principale = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ==== Connecter le socket ==== Pour une connexion serveur, qui va attendre des connexions de clients, on utilise la méthode **bind**. Elle prend un paramètre : le tuple (nom_hote, port). Mais pour que le serveur écoute sur un port, il faut le configurer en conséquence en ne renseignant pas le nom de l'hôte sera vide mais en précisant un numéro de port 1024 et 65535. >>> connexion_principale.bind(('', 12800)) ==== Faire écouter le socket ==== Le socket est prêt à écouter sur le port 12800 mais il n'écoute pas encore. On va avant tout lui préciser le nombre maximum de connexions qu'il peut recevoir sur ce port sans les accepter. On utilise pour cela la méthode **listen**. On lui passe généralement 5 en paramètre. Cela ne veut pas dire que le serveur ne pourra dialoguer qu'avec 5 clients maximum. Cela veut dire que si 5 clients se connectent et que le serveur n'accepte aucune de ces connexions, aucun autre client ne pourra se connecter. Mais généralement, très peu de temps après que le client ait demandé la connexion, le serveur l'accepte. Vous pouvez donc avoir bien plus de clients connectés. >>> connexion_principale.listen(5) ==== Accepter une connexion venant du client ==== Dernière étape, accepter une connexion. Aucune connexion ne s'est encore présentée mais la méthode accept qui va être utilisée va bloquer le programme tant qu'aucun client ne s'est connecté. La méthode **accept** renvoie deux informations : * le socket connecté qui vient de se créer, celui qui va nous permettre de dialoguer avec notre client tout juste connecté ; * un **tuple** représentant l'adresse IP et le port de connexion du client. Le port de connexion du client n'est pas le même que celui du serveur, car le client, en ouvrant une connexion, passe par un port client qui va être choisi par le système parmi les ports disponibles. Il y a donc deux ports mis en œuvre dans une connexion TCP. >>> connexion_avec_client, infos_connexion = connexion_principale.accept() Cette méthode bloque le programme. Elle attend qu'un client se connecte. Laissez cette fenêtre Python ouverte et, à présent, ouvrez-en une nouvelle pour construire notre client. ===== Création du client ===== Créez votre socket de la même façon : >>> import socket >>> connexion_avec_serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ==== Connecter le client ==== Pour se connecter à un serveur, on utilise la méthode **connect** qui prend en paramètre un tuple contenant le nom d'hôte (actuellement **localhost**) et le numéro du port identifiant le serveur (actuellement 12800 ) auquel on veut se connecter. >>> connexion_avec_serveur.connect(('localhost', 12800)) Maintenant votre serveur et votre client sont connectés ! Si vous retournez dans la console Python abritant le serveur, vous pouvez constater que la méthode **accept** ne bloque plus, puisqu'elle vient d'accepter la connexion demandée par le client. Vous pouvez donc de nouveau saisir du code côté serveur : >>> print(infos_connexion) ('127.0.0.1', 2901) La première information, c'est l'adresse IP du client. Ici, elle vaut 127.0.0.1 c'est-à-dire l'IP de l'ordinateur local. Le second est le port de sortie du client (cette information peut être différente pour vous). ===== Faire communiquer les sockets ===== Pour faire communiquer les **sockets**, on utilise les méthodes **send** pour envoyer et **recv** pour recevoir. Les informations que vous transmettrez doivent être des chaînes de **bytes** et non pas de type **str** ! ==== Côté serveur : ==== >>> connexion_avec_client.send(b"Je viens d'accepter la connexion") 32 La méthode **send** vous renvoie le nombre de caractères envoyés c'est à dire **32**. Maintenant, côté client, on va réceptionner le message que l'on vient d'envoyer. La méthode **recv** prend en paramètre le nombre de caractères à lire. Généralement, on lui passe la valeur **1024**. Si le message est plus grand que 1024 caractères, on récupérera le reste après. Dans la fenêtre Python côté client : >>> msg_recu = connexion_avec_serveur.recv(1024) >>> print(msg_recu) b"Je viens d'accepter la connexion" Vous pouvez de cette manière faire communiquer des applications entre elles, applications pouvant être situées sur des ordinateurs différents. Le client peut également envoyer des informations au serveur et le serveur peut les réceptionner, tout cela grâce à ces méthodes **send** et **recv**. ===== Fermer la connexion ===== Pour fermer la connexion, il faut appeler la méthode **close** de notre socket. ==== Côté serveur : ==== >>> connexion_avec_client.close() ==== Côté client : ==== >>> connexion_avec_serveur.close() ===== Premier programme serveur ===== Voici un premier programme serveur qui n'accepte qu'un seul client et qui fonctionne jusqu'à recevoir du client le message **fin**. Enregistrez ce programme dans le fichier **serveur.py** puis lancez-le (**Menu Run / Run module ou bien F5**). À chaque message reçu, le serveur envoie en retour le message 'OK'. import socket hote = '' port = 12800 connexion_principale = socket.socket(socket.AF_INET, socket.SOCK_STREAM) connexion_principale.bind((hote, port)) connexion_principale.listen(5) print("Le serveur écoute à présent sur le port {}".format(port)) connexion_avec_client, infos_connexion = connexion_principale.accept() msg_recu = b"" while msg_recu != b"fin": msg_recu = connexion_avec_client.recv(1024) # L'instruction ci-dessous peut lever une exception si le message # Réceptionné comporte des accents print(msg_recu.decode()) connexion_avec_client.send(b"OK") print("Fermeture de la connexion") connexion_avec_client.close() connexion_principale.close() ===== Premier programme client ===== Le programme client va tenter de se connecter sur le port 12800 de la machine locale. Il demande à l'utilisateur de saisir quelque chose au clavier et envoie ce quelque chose au serveur, puis attend sa réponse. Enregistrez ce programme dans le fichier **client.py** puis lancez-le. import socket hote = "localhost" port = 12800 connexion_avec_serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM) connexion_avec_serveur.connect((hote, port)) print("Connexion établie avec le serveur sur le port {}".format(port)) msg_a_envoyer = b"" while msg_a_envoyer != b"fin": msg_a_envoyer = input("> ") # Peut planter si vous tapez des caractères spéciaux msg_a_envoyer = msg_a_envoyer.encode() # On envoie le message connexion_avec_serveur.send(msg_a_envoyer) msg_recu = connexion_avec_serveur.recv(1024) print(msg_recu.decode()) # Là encore, peut planter s'il y a des accents print("Fermeture de la connexion") connexion_avec_serveur.close() La méthode **encode** de **str** prend en paramètre un nom d'encodage et transforme une chaîne **str** en chaîne **bytes**. Cela est nécessaire car la méthode **send** n'accepte que le type de données bytes. Cela se fait en fonction d'un encodage précis (par défaut, Utf-8). La méthode **decode**, à l'inverse, est une méthode de **bytes**. Elle aussi peut prendre en paramètre un encodage et elle renvoie une chaîne **str** décodée grâce à l'encodage (par défaut Utf-8). Sous Windows utilisez de préférence l'encodage **Latin-1**. ===== Deuxième programme serveur ===== Ce qui sera amélioré : * accepter plusieurs clients ; * pouvoir envoyer ou recevoir plusieurs messages sans attendre un accusé de réception ; * gérer les erreurs. ==== Le module select ==== Le module **select** permet d'interroger plusieurs clients dans l'attente d'un message à réceptionner, sans paralyser le programme. Select va écouter sur une liste de clients et retourner au bout d'un temps précisé. Ce que renvoie select, c'est la liste des clients qui ont un message à réceptionner. Il suffit de parcourir ces clients, de lire les messages en attente (grâce à recv). Utilisation de la fonction **select** du module **select** La fonction select prend trois ou quatre arguments et en renvoie trois. * rlist : la liste des sockets en attente d'être lus ; * wlist : la liste des sockets en attente d'être écrits ; * xlist : la liste des sockets en attente d'une erreur ; * timeout : le délai pendant lequel la fonction attend avant de retourner. Si vous précisez en timeout 0, la fonction retourne immédiatement. Si ce paramètre n'est pas précisé, la fonction retourne dès qu'un des sockets change d'état (est prêt à être lu s'il est dans **rlist** par exemple) mais pas avant. Pour ce programme vous allez utiliser **rlist** et **timeout**. Il s'agit de mettre des **sockets** dans une liste et que **select** les surveille, en retournant dès qu'un socket est prêt à être lu. Comme cela votre programme ne bloque pas et il peut recevoir des messages de plusieurs clients dans un ordre complètement inconnu. Si vous ne le précisez pas le **timeout**, **select** bloque jusqu'au moment où l'un des **sockets** que vous écoutez est prêt à être lu. Si vous précisez un **timeout** de **0**, **select** retournera tout de suite. Sinon, select retournera au bout du temps que vous indiquez en secondes, ou plus tôt si un socket est prêt à être lu. Si vous précisez un **timeout** de **1**, la fonction va bloquer pendant une seconde maximum. Mais si un des sockets en écoute est prêt à être lu dans l'intervalle (c'est-à-dire si un des clients envoie un message au serveur), la fonction retourne prématurément. select renvoie trois listes (**rlist**, **wlist** et **xlist**), sauf qu'il ne s'agit pas des listes fournies en entrée mais uniquement des sockets **à lire** dans le cas de **rlist**. **Exemple :** rlist, wlist, xlist = select.select(clients_connectes, [], [], 0.05) Cette instruction va écouter les sockets contenus dans la liste **clients_connectes**. Elle retournera au plus tard dans 50 millisecondes. Mais elle retournera plus tôt si un client envoie un message. La liste des clients ayant envoyé un message se retrouve dans la variable **rlist**. On la parcourt ensuite et on peut appeler **recv** sur chacun des sockets. **N'hésitez pas à voir la documentation du module select** === Modification du programme du serveur (le programme du client ne change pas) === Vous allez créer un serveur pouvant accepter plusieurs clients, réceptionner leurs messages et leur envoyer une confirmation à chaque réception. Vous allez rajouter l'utilisation de **select** pour travailler avec plusieurs clients. select va permettre : * d'écouter plusieurs clients connectés * de savoir si un (ou plusieurs) clients sont connectés au serveur. Rappel : la méthode accept est aussi une fonction bloquante. import socket import select hote = '' port = 12800 connexion_principale = socket.socket(socket.AF_INET, socket.SOCK_STREAM) connexion_principale.bind((hote, port)) connexion_principale.listen(5) print("Le serveur écoute à présent sur le port {}".format(port)) serveur_lance = True clients_connectes = [] while serveur_lance: # vérifier que de nouveaux clients ne demandent pas à se connecter # Pour cela, on écoute la connexion_principale en lecture # On attend maximum 50ms connexions_demandees, wlist, xlist = select.select([connexion_principale], [], [], 0.05) for connexion in connexions_demandees: connexion_avec_client, infos_connexion = connexion.accept() # On ajoute le socket connecté à la liste des clients clients_connectes.append(connexion_avec_client) # Maintenant, on écoute la liste des clients connectés # Les clients renvoyés par select sont ceux devant être lus (recv) # On attend là encore 50ms maximum # On enferme l'appel à select.select dans un bloc try # En effet, si la liste de clients connectés est vide, une exception # Peut être levée clients_a_lire = [] try: clients_a_lire, wlist, xlist = select.select(clients_connectes, [], [], 0.05) except select.error: pass else: # On parcourt la liste des clients à lire for client in clients_a_lire: # Client est de type socket msg_recu = client.recv(1024) # Peut planter si le message contient des caractères spéciaux msg_recu = msg_recu.decode() print("Reçu {}".format(msg_recu)) client.send(b"OK") if msg_recu == "fin": serveur_lance = False print("Fermeture des connexions") for client in clients_connectes: client.close() connexion_principale.close() Maintenant le serveur peut accepter des connexions de plus d'un client et ne se bloque pas dans l'attente d'un message, du moins pas plus de 50 millisecondes. Les déconnexions fortuites ne sont pas gérées non plus. === Pour aller plus loin : === Regarder la documentation du module **socket**, de **select** et de **socketserver**. Le module **socketserver**, propose une alternative pour monter vos applications serveur. Il en existe d'autres. Vous pouvez utiliser des sockets non bloquants (c'est-à-dire qui ne bloquent pas le programme quand vous utilisez leur méthode **accept** ou **recv**) ou des **threads** pour exécuter différentes portions de votre programme en parallèle.