@@ -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 | |||
15-fichiers-et-données-binaires/index | |||
16-interpréteur-interactif/index | |||
17-sockets/index |