@@ -0,0 +1,128 @@ | |||||
Exemple | |||||
======= | |||||
Pour ce premier exemple, le client et le serveur vont tourner sur la même machine: la vôtre! | |||||
Le processus du serveur tournera dans un premier terminal, et le processus du client dans un autre. | |||||
Pour les différencier, on peut utiliser la variable `sys.ps1`. | |||||
Étape 1 | |||||
------- | |||||
Ouvrez deux terminaux, lancez dans chacun d'eux la commande `python3` sans arguments, puis tapez:: | |||||
>>> import sys | |||||
>>> sys.ps1 = "(serveur) >>> " | |||||
dans le premier, et:: | |||||
>>> import sys | |||||
>>> sys.ps1 = "(client) >>> " | |||||
dans le second. | |||||
Vous devriez obtenir le résultat suivant:: | |||||
(serveur) >>> | |||||
:: | |||||
(client) >>> | |||||
Dans chacun d'eux, définissez les variables ``IP`` et ``PORT``:: | |||||
(serveur) >>> IP = "127.0.0.1" | |||||
(serveur) >>> PORT = 3000 | |||||
:: | |||||
(client) >>> IP = "127.0.0.1" | |||||
(client) >>> PORT = 3000 | |||||
L'adresse IP ``127.0.0.1`` est spéciale et désigne votre propre machine. | |||||
Le PORT 3000 est un port arbitraire | |||||
Étape 2 | |||||
------- | |||||
Dans le REPL du serveur, créez une socket du type 'AF_INET' et 'SOCK_STREAM', puis appelez | |||||
``bind()`` avec le tuple (IP, PORT) [#f1]_ :: | |||||
(serveur) >>> IP = "127.0.0.1" | |||||
(serveur) >>> PORT = 3000 | |||||
(serveur) >>> import socket | |||||
(serveur) >>> s_serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |||||
(serveur) >>> s_serveur.bind((IP, PORT)) | |||||
Ensuite, appelez ``s_serveur.listen(0)``: cela permet à votre serveur d'accepter des connections:: | |||||
(serveur) >>> s_serveur.listen(0) | |||||
Enfin, appelez ``s_serveur.accept`: cette méthode retourne un tuple qu'on note souvent ``con, addr``:: | |||||
(serveur) >>> con, addr = s_serveur.accept() | |||||
Cette fois ci, vous devriez constater que le processus du serveur est *bloqué*: l'invite de commande ne s'affiche | |||||
pas - en effet, le serveur est en attente d'une connexion par le client | |||||
Étape 3 | |||||
------- | |||||
De la même façon que pour le serveur, créez une socket du même type côté | |||||
client:: | |||||
(client) >>> IP = "127.0.0.1" | |||||
(client) >>> PORT = 3000 | |||||
(client) >>> import socket | |||||
(client) >>> s_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |||||
Ensuite, *connectez* la socket client à la socket serveur:: | |||||
(client) >>> s_client.connect((IP, PORT)) | |||||
Comme par magie, vous devriez voir le processus **dans l'autre terminal** reprendre son exécution: | |||||
les deux processus Python sont donc bien en train de *communiquer* | |||||
Étape 4 | |||||
------- | |||||
Vous pouvez maintenant utiliser les méthodes ``send()`` et ``recv``, respectivement avec l'objet | |||||
``con`` côté serveur, et ``s_client`` côté client pour envoyer et recevoir des messages entre | |||||
les deux processus. | |||||
Notez que ``send`` prend des ``bytes`` en arguments et renvoie le nombre d'octets envoyés, | |||||
et que ``recv()`` prend un nombre d'octets à lire. | |||||
On peut envoyer des message du client vers le serveur:: | |||||
(client) >>> s_client.send(b"Bonjour") | |||||
7 | |||||
:: | |||||
(serveur) >>> con.recv(7) | |||||
b'Bonjour' | |||||
Et du serveur vers le client:: | |||||
(serveur) >>> con.send(b"Comment va ?") | |||||
12 | |||||
:: | |||||
(client) >>> s_client.recv(12) | |||||
b'Comment va ?' | |||||
.. rubric:: notes | |||||
.. [#f1] Il existe des sockets de plusieurs type et avec des comportements différents. AF_INET et SOCK_STREAM sont | |||||
les plus courants. |
@@ -0,0 +1,14 @@ | |||||
Chapitre 17 - Sockets | |||||
===================== | |||||
Ce chapitre n'est pas à proprement parler un cours sur le langage | |||||
Python, mais contient des éléments qui nous servirons à bâtir | |||||
un projet à long terme : fabriquer un site Web ! | |||||
.. toctree:: | |||||
:maxdepth: 1 | |||||
introduction | |||||
exemple | |||||
protocoles |
@@ -0,0 +1,28 @@ | |||||
Introduction | |||||
============= | |||||
Clients et serveurs | |||||
------------------- | |||||
Dans le chapitre 15, nous avons parlé des données binaires et évoqué le | |||||
fait que cela permettait à nos programmes Python d'intéragir avec | |||||
l'extérieur, notamment via le système de fichiers. | |||||
Il existe dans la librairie standard une autre façon pour Python de | |||||
communiquer via *le réseau* : il s'agit du module ``socket``. | |||||
Une communication sur le réseau Internet implique: | |||||
* Un *client* qui va faire des *requêtes* | |||||
* Un *serveur* qui va renvoyer des *réponses* au client | |||||
* Une *adresse* du serveur utilisée par le client | |||||
Dans notre cas, cette adresse est un *tuple*, composé de deux éléments: | |||||
une *adresse IP* et un *port*. | |||||
.. image:: /img/client-serveur.png | |||||
En général: | |||||
* Le *client* et le *serveur* sont sur des machines différentes | |||||
* Il n'y a qu'un seul processus qui est capable d'écouter sur un port donné |
@@ -0,0 +1,145 @@ | |||||
Protocoles | |||||
========== | |||||
Le problème | |||||
------------ | |||||
Notez que ``send()`` et ``recv()`` sont très basiques: | |||||
* On peut appeler ``send()`` plusieurs fois d'affilée | |||||
* On peut aussi appeler ``recv()`` alors que le client n'a encore rien envoyé, | |||||
le serveur va juste bloquer en attendant le prochain message du client. | |||||
* Si le client envoie 3 octets et que le serveur utilise ``recv(10)``, il n'y | |||||
a pas d'erreur | |||||
* Si le client envoie 10 octets et que le serveur utilise ``recv(3)``, il n'y | |||||
a pas non plus d'erreur, et il faut appeler ``recv()`` une deuxième fois | |||||
(avec 7 par exemple) pour récupérer le message en entier | |||||
Protocoles | |||||
----------- | |||||
Ainsi, si on veut que deux machines s'échangent des messages via Internet, il faut | |||||
convenir d'un **protocole** - qui parle en premier, quel genre de message peut-il | |||||
envoyer? Il faut aussi convenir d'une façon de communiquer la *taille* des réponses. | |||||
Le protocole HTTP | |||||
----------------- | |||||
Le protocole HTTP est relativement simple. On peut le vérifier en essayant | |||||
de se connecter à l'adresse ``(93.184.216.34, 80)``, et envoyant les messages | |||||
suivants - à l'heure où j'écris ces lignes, l'IP ci-dessus correspond au site | |||||
``http://example.com`` :: | |||||
import socket | |||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |||||
s.connect(("93.184.216.34", 80)) | |||||
s.send(b"GET / HTTP/1.1\r\n") | |||||
s.send(b"Host: example.com\r\n") | |||||
s.send(b"\r\n") | |||||
message = s.recv(351).decode() | |||||
print(message) | |||||
Résultat: | |||||
.. code-block:: text | |||||
HTTP/1.1 200 OK | |||||
Age: 514391 | |||||
Cache-Control: max-age=604800 | |||||
Content-Type: text/html; charset=UTF-8 | |||||
Date: Sun, 01 Mar 2020 15:57:23 GMT | |||||
Etag: "3147526947+ident" | |||||
Expires: Sun, 08 Mar 2020 15:57:23 GMT | |||||
Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT | |||||
Server: ECS (bsa/EB18) | |||||
Vary: Accept-Encoding | |||||
X-Cache: HIT | |||||
Content-Length: 1256 | |||||
<!doctype html> | |||||
Notez qu'on a appelé `.decode()` sur le message reçu du serveur - on dit que HTTP | |||||
est un protocole de *texte*. | |||||
Notez que la réponse du serveur rappelle le nom du protocole ``HTTP/1.1`` et répond avec | |||||
de nombreuses lignes contenant une *clé* et une *valeur* séparé par des deux-points | |||||
On appelle cela des *en-têtes* (ou *headers*). L'un d'eux contient la taille totale du | |||||
message: ``Content-Length: 1256``. | |||||
Après le bloc de headers, on voit le *corps* de la réponse: quelque chose qui commence | |||||
par ``<!doctype html>``. | |||||
On peut lire la suite du message:: | |||||
suite = s.recv(1000).decode() | |||||
print(suite) | |||||
.. code-block:: text | |||||
<html> | |||||
<head> | |||||
<title>Example Domain</title> | |||||
<meta charset="utf-8" /> | |||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" /> | |||||
<meta name="viewport" content="width=device-width, initial-scale=1" /> | |||||
<style type="text/css"> | |||||
body { | |||||
background-color: #f0f0f2; | |||||
margin: 0; | |||||
padding: 0; | |||||
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; | |||||
} | |||||
div { | |||||
width: 600px; | |||||
margin: 5em auto; | |||||
padding: 2em; | |||||
background-color: #fdfdff; | |||||
border-radius: 0.5em; | |||||
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02); | |||||
} | |||||
a:link, a:visited { | |||||
color: #38488f; | |||||
text-decoration: none; | |||||
} | |||||
@media (max-width: 700px) { | |||||
div { | |||||
margin: 0 auto; | |||||
width: auto; | |||||
} | |||||
} | |||||
</style> | |||||
</head> | |||||
<body> | |||||
<div> | |||||
<h1>Example Domain</h1> | |||||
<p>This domain is for use in illustrative examples in documents. You may use this | |||||
domain in literature without prior coordination or asking for permission.</p> | |||||
<p><a href="https://www.iana.org/domains/example">More information...</a></p> | |||||
</div> | |||||
</body> | |||||
</html> | |||||
Si maintenant vous allez sur ``https://example.com`` avec un navigateur Web, et cliquez sur | |||||
"Afficher le code source de la page", vous devriez voir exactement le contenu ci-dessus. | |||||
Cela prouve que: | |||||
* Il y a un serveur qui écoute sur l'adresse IP de example.com, sur le port 80 | |||||
* Ce serveur comprend le protocole HTTP | |||||
* Un navigateur ne fait rien d'autre que: | |||||
* envoyer des requêtes HTTP vers un serveur | |||||
* interpréter le texte retourné et l'afficher | |||||
* On peut facilement coder à la fois des *clients* et des *serveur* HTTP en Python, juste avec le module socket. | |||||
@@ -33,3 +33,4 @@ remarques. | |||||
14-bibliothèques-01/index | 14-bibliothèques-01/index | ||||
15-fichiers-et-données-binaires/index | 15-fichiers-et-données-binaires/index | ||||
16-interpréteur-interactif/index | 16-interpréteur-interactif/index | ||||
17-sockets/index |