Académique Documents
Professionnel Documents
Culture Documents
Vaste sujet que le réseau ! Si je devais faire une présentation détaillée, ou même parler des
réseaux en général, il me faudrait bien plus d'un chapitre rien que pour la théorie.
Dans ce chapitre, nous allons donc apprendre à faire communiquer deux applications grâce
aux sockets, des objets qui permettent de connecter un client à un serveur et de transmettre des
données de l'un à l'autre.
Il existe plusieurs protocoles de communication en réseau. Si vous voulez, c'est un peu comme
la communication orale : pour que les échanges se passent correctement, les deux (ou plus)
parties en présence doivent parler la même langue. Nous allons ici parler du protocole TCP.
Le protocole TCP
L'acronyme de ce protocole signifie Transmission Control Protocol, soit « protocole de contrôle
de transmission ». Concrètement, il permet de connecter deux applications et de leur faire
échanger des informations.
Ce protocole est dit « orienté connexion », c'est-à-dire que les applications sont connectées pour
communiquer et que l'on peut être sûr, quand on envoie une information au travers du réseau,
qu'elle a 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.
Cela vous paraît peut-être évident mais le protocole UDP (User Datagram Protocol), par
exemple, envoie des informations au travers du réseau sans se soucier de savoir si elles seront
bien réceptionnées par la cible. Ce protocole n'est pas connecté, une application envoie quelque
chose au travers du réseau en spécifiant une cible. Il suffit alors de prier très fort pour que le
message soit réceptionné correctement !
Plus sérieusement, ce type de protocole est utile si vous avez besoin de faire transiter beaucoup
d'informations au travers du réseau mais qu'une petite perte occasionnelle d'informations n'est
pas très handicapante. On trouve ce type de protocole dans des jeux graphiques en réseau, le
serveur envoyant très fréquemment des informations au client pour qu'il actualise sa fenêtre.
Cela fait beaucoup à transmettre mais ce n'est pas dramatique s'il y a une petite perte
d'informations de temps à autre puisque, quelques millisecondes plus tard, le serveur renverra de
nouveau les informations.
En attendant, c'est le protocole TCP qui nous intéresse. Il est un peu plus lent que le
protocole UDP mais plus sûr et, pour la quantité d'informations que nous allons transmettre, il est
préférable d'être sûr des informations transmises plutôt que de la vitesse de transmission.
Clients et serveur
Dans l'architecture que nous allons voir dans ce chapitre, on trouve en général un serveur et
plusieurs clients. Le serveur, c'est une machine qui va traiter les requêtes du client.
Si vous accédez par exemple à OpenClassrooms, c'est parce que votre navigateur, faisant office
de client, se connecte au serveur d'OpenClassrooms. Il lui envoie un message en lui demandant
la page que vous souhaitez afficher et le serveur d'OpenClassrooms, dans sa grande bonté,
envoie la page demandée au client.
Cette architecture est très fréquente, même si ce n'est pas la seule envisageable.
Dans les exemples que nous allons voir, nous allons créer deux applications :
l'application serveur et l'application client. Le serveur écoute donc en attendant des connexions
et les clients se connectent au serveur.
Le serveur :
1. se connecte au serveur ;
2. échange des informations avec le serveur ;
3. ferme la connexion.
Comme on l'a vu, le serveur peut dialoguer avec plusieurs clients : c'est tout l'intérêt. Si le
serveur d'OpenClassrooms ne pouvait dialoguer qu'avec un seul client à la fois, il faudrait
attendre votre tour, peut-être assez longtemps, avant d'avoir accès à vos pages. Et, sans serveur
pouvant dialoguer avec plusieurs clients, les jeux en réseau ou les logiciels de messagerie
instantanée seraient bien plus complexes.
Le nom d'hôte (host name en anglais), qui identifie une machine sur Internet ou sur un
réseau local. Les noms d'hôtes permettent de représenter des adresses IP de façon plus
claire (on a un nom comme google.fr, plus facile à retenir que l'adresse IP
correspondante 74.125.224.84).
Un numéro de port, qui est souvent propre au type d'information que l'on va échanger. Si
on demande une connexion web, le navigateur va en général interroger le port 80 si c'est
en http ou le port 443 si c'est en connexion sécurisée (https). Le numéro de port est
compris entre 0 et 65535 (il y en a donc un certain nombre !) et les numéros entre 0 et
1023 sont réservés par le système. On peut les utiliser, mais ce n'est pas une très bonne
idée.
Pour résumer, quand votre navigateur tente d'accéder à OpenClassrooms, il établit une
connexion avec le serveur dont le nom d'hôte est fr.openclassrooms.com sur le port 80. Dans ce
chapitre, nous allons plus volontiers travailler avec des noms d'hôtes qu'avec des adresses IP.
Les sockets
Comme on va le voir, 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 et nous allons maintenant voir comment ils
fonctionnent.
Les sockets
Commençons donc, dans la joie et la bonne humeur, par importer notre module socket.
import socket
Nous allons d'abord créer notre serveur puis, en parallèle, un client. Nous allons faire
communiquer les deux. Pour l'instant, nous nous occupons du serveur.
>>>
Connecter le socket
Ensuite, nous connectons notre 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).
Attends un peu, je croyais que c'était notre client qui se connectait à notre serveur, pas
l'inverse…
Oui mais, pour que notre serveur écoute sur un port, il faut le configurer en conséquence. Donc,
dans notre cas, le nom de l'hôte sera vide et le port sera celui que vous voulez, entre 1024 et
65535.
>>>
Faire écouter notre socket
Bien. Notre 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 5en paramètre.
Cela veut dire que notre serveur ne pourra dialoguer qu'avec 5 clients maximum ?
Non. 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, ne vous en faites pas.
>>> connexion_principale.listen(5)
>>>
Accepter une connexion venant du client
Enfin, dernière étape, on va accepter une connexion. Aucune connexion ne s'est encore
présentée mais la méthode accept que nous allons utiliser va bloquer le programme tant
qu'aucun client ne s'est connecté.
Création du client
Commencez par construire votre socket de la même façon :
>>>
Connecter le client
Pour se connecter à un serveur, on va utiliser la méthode connect. Elle prend en paramètre un
tuple, comme bind, contenant le nom d'hôte et le numéro du port identifiant le serveur auquel on
veut se connecter.
Le numéro du port sur lequel on veut se connecter, vous le connaissez : c'est 12800. Vu que nos
deux applications Python sont sur la même machine, le nom d'hôte va être localhost (c'est-à-
dire la machine locale).
>>>
Et voilà, notre serveur et notre 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. Dites-vous que l'hôte localhost redirige vers l'IP 127.0.0.1.
Le second est le port de sortie du client, qui ne nous intéresse pas ici.
Les informations que vous transmettrez seront des chaînes de bytes, pas des str !
Donc côté serveur :
32
>>>
La méthode send vous renvoie le nombre de caractères envoyés.
>>> msg_recu
>>>
Magique, non ? Vraiment pas ? Songez que ce petit mécanisme peut servir à faire communiquer
des applications entre elles non seulement sur la machine locale, mais aussi sur des machines
distantes et reliées par Internet.
Le client peut également envoyer des informations au serveur et le serveur peut les réceptionner,
tout cela grâce aux méthodes send et recv que nous venons de voir.
Fermer la connexion
Pour fermer la connexion, il faut appeler la méthode close de notre socket.
Côté serveur :
>>> connexion_avec_client.close()
>>>
Et côté client :
>>> connexion_avec_serveur.close()
>>>
Voilà ! Je vais récapituler en vous présentant dans l'ordre un petit serveur et un client que nous
pouvons utiliser. Et pour finir, je vous montrerai une façon d'optimiser un peu notre serveur en lui
permettant de gérer plusieurs clients à la fois.
Le serveur
Pour éviter les confusions, je vous remets ici le code du serveur, légèrement amélioré. Il
n'accepte qu'un seul client (nous verrons plus bas comment en accepter plusieurs) et il tourne
jusqu'à recevoir du client le message fin.
À chaque fois que le serveur reçoit un message, il envoie en retour le message '5 / 5'.
import socket
hote = ''
port = 12800
connexion_principale.bind((hote, port))
connexion_principale.listen(5)
msg_recu = b""
msg_recu = connexion_avec_client.recv(1024)
print(msg_recu.decode())
connexion_avec_client.send(b"5 / 5")
print("Fermeture de la connexion")
connexion_avec_client.close()
connexion_principale.close()
Voilà pour le serveur. Il est minimal, vous en conviendrez, mais il est fonctionnel. Nous verrons
un peu plus loin comment l'améliorer.
Le client
Là encore, je vous propose le code du client pouvant interagir avec notre serveur.
Il 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.
import socket
hote = "localhost"
port = 12800
connexion_avec_serveur.connect((hote, port))
msg_a_envoyer = b""
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("Fermeture de la connexion")
connexion_avec_serveur.close()
Que font les méthodes encode et decode ?
encode est une méthode de str. Elle peut prendre en paramètre un nom d'encodage et permet
de passer un str en chaîne bytes. C'est, comme vous le savez, ce type de chaîne
que send accepte. En fait, encodeencode la chaîne str en fonction d'un encodage précis (par
défaut, Utf-8).
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).
Si l'encodage de votre console est différent d'Utf-8 (ce sera souvent le cas sur Windows), des
erreurs peuvent se produire si les messages que vous encodez ou décodez comportent des
accents.
Voilà, nous avons vu un serveur et un client, tous deux très simples. Maintenant, voyons quelque
chose de plus élaboré !
o D'abord, notre serveur ne peut accepter qu'un seul client. Si d'autres clients veulent
se connecter, ils ne peuvent pas.
o Ensuite, on part toujours du principe qu'on attend le message d'un client et qu'on lui
renvoie immédiatement après réception. Mais ce n'est pas toujours le cas : parfois
vous envoyez un message au client alors qu'il ne vous a rien envoyé, parfois vous
recevez des informations de sa part alors que vous ne lui avez rien envoyé.
Prenez un logiciel de messagerie instantanée : est-ce que, pour dialoguer, vous êtes obligés
d'attendre que votre interlocuteur vous réponde ? Ce n'est pas « j'envoie un message, il me
répond, je lui réponds, il me répond »… Parfois, souvent même, vous enverrez deux messages à
la suite, peut-être même trois, ou l'inverse, qui sait ? Bref, on doit pouvoir envoyer plusieurs
messages au client et réceptionner plusieurs messages dans un ordre inconnu. Avec notre
technique, c'est impossible (faites le test si vous voulez).
Le module select
Le module select va nous permettre une chose très intéressante, à savoir interroger plusieurs
clients dans l'attente d'un message à réceptionner, sans paralyser notre programme.
Pour schématiser, 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) et le tour est joué.
Sur Linux, select peut être utilisé sur autre chose que des sockets mais, cette fonctionnalité
n'étant pas portable, je ne fais que la mentionner ici.
En théorie
La fonction qui nous intéresse porte le même nom que le module associé, select. Elle prend
trois ou quatre arguments et en renvoie trois. C'est maintenant qu'il faut être attentif :
select renvoie trois listes, là encore 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.
Ce n'est pas clair ? Considérez cette ligne (ne l'essayez pas encore ) :
Si ce n'est pas plus clair, rassurez-vous : nous allons voir select en action un peu plus bas. Vous
pouvez également aller jeter un coup d'œil à la documentation du module select.
select en action
Nous allons un peu travailler sur notre serveur. Vous pouvez garder le même client de test.
Le but va être de créer un serveur pouvant accepter plusieurs clients, réceptionner leurs
messages et leur envoyer une confirmation à chaque réception. L'exercice ne change pas
beaucoup mais on va utiliser select pour travailler avec plusieurs clients.
J'ai parlé de select pour écouter plusieurs clients connectés mais cette fonction va également
nous permettre de savoir si un (ou plusieurs) clients sont connectés au serveur. Si vous vous
souvenez, la méthode accept est aussi une fonction bloquante. On va du reste l'utiliser de la
même façon qu'un peu plus haut.
Je crois vous avoir donné assez d'informations théoriques. Le code doit parler maintenant :
import socket
import select
hote = ''
port = 12800
serveur_lance = True
clients_connectes = []
while serveur_lance:
# On va 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)
connexion_principale.close()
C'est plus long hein ? C'est inévitable, cependant.
Maintenant notre serveur peut accepter des connexions de plus d'un client, vous pouvez faire le
test. En outre, il ne se bloque pas dans l'attente d'un message, du moins pas plus de 50
millisecondes.
Je pense que les commentaires sont assez précis pour vous permettre d'aller plus loin. Ceci
n'est naturellement pas encore une version complète mais, grâce à cette base, vous devriez
pouvoir facilement arriver à quelque chose. Pourquoi ne pas faire un mini tchat ?
Les déconnexions fortuites ne sont pas gérées non plus. Mais vous avez assez d'éléments pour
faire des tests et améliorer notre serveur si cela vous tente.
Et encore plus
Je vous l'ai dit, le réseau est un vaste sujet et, même en se restreignant au sujet que j'ai choisi, il
y aurait beaucoup d'autres choses à vous montrer. Je ne peux tout simplement pas remplacer la
documentation et donc, si vous voulez en apprendre plus, je vous invite à jeter un coup d'œil à la
page du module socket, de select et de socketserver.
Le dernier module, socketserver, propose une alternative pour monter vos applications serveur.
Il en existe d'autres, dans tous les cas : 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. Mais je vous
laisse vous documenter sur ces sujets s'ils vous intéressent !
En résumé
Dans la structure réseau que nous avons vue, on trouve un serveur pouvant dialoguer
avec plusieurs clients.
Pour créer une connexion côté serveur ou client, on utilise le module socket et la
classe socket de ce module.
Pour se connecter à un serveur, le socket client utilise la méthode connect.
Pour écouter sur un port précis, le serveur utilise d'abord la méthode bind puis la
méthode listen.
Pour s'échanger des informations, les sockets client et serveur utilisent les
méthodes send et recv.
Pour fermer une connexion, le socket serveur ou client utilise la méthode close.
Le module select peut être utile si l'on souhaite créer un serveur pouvant gérer plusieurs
connexions simultanément ; toutefois, il en existe d'autres.