9.13. Sockets TCP

Les sockets TCP se présentent sous deux formes qui semblent différentes mais partagent le même type sous-jacent : les sockets client qui se connectent (connect()) à un serveur distant, et les sockets serveur qui se lient à une adresse (bind()), écoutent (listen()) et acceptent (accept()) les connexions entrantes. Les deux rôles utilisent la même classe socket présentée sur Objets socket ; seules les méthodes appelées sur eux diffèrent.

9.13.1. Un client TCP

Le client le plus simple ouvre une connexion, envoie une requête, lit la réponse et ferme la connexion : :

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.1.20", 9000))

s.send(b"hello\n")
reply = s.recv(1024)
print("reply:", reply)

s.close()

connect() exécute la poignée de main en trois temps abordée sur TCP – un flux d’octets fiable et retourne lorsque la connexion est ouverte. send() écrit des octets dans la connexion ; recv() y lit jusqu’à un nombre donné d’octets. Une fois l’application terminée, close() ferme la connexion.

Le même script encadré par l’idiome de l’instruction with présenté sur Objets socket, de sorte que le socket est fermé même si une exception est levée : :

import socket

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect(("192.168.1.20", 9000))
    s.send(b"hello\n")
    print(s.recv(1024))

9.13.1.1. Lire jusqu’à la fin

Un seul appel à recv() retourne au plus le nombre d’octets demandé – il peut en retourner moins, car TCP est un flux plutôt qu’une séquence de messages. L’application doit continuer à lire jusqu’à obtenir la réponse complète : :

chunks = []
while True:
    chunk = s.recv(1024)
    if not chunk:                  # empty bytes -> other side closed
        break
    chunks.append(chunk)
reply = b"".join(chunks)

La boucle se termine lorsque recv() retourne un objet bytes vide. Cela se produit lorsque l’autre extrémité a proprement fermé sa moitié de la connexion ; l’application interprète la « fin du flux » comme étant la « fin du message » dans ce style de protocole.

9.13.1.2. Envoyer jusqu’à la fin

La mise en garde inverse s’applique à send() : elle peut envoyer moins d’octets que demandé, en retournant le nombre d’octets réellement écrits. Pour les charges utiles volumineuses, réessayez avec le reste non envoyé : :

payload = some_big_bytes
while payload:
    n = s.send(payload)
    payload = payload[n:]

sendall() effectue la boucle en interne, de sorte que la plupart du code peut simplement l’appeler et éviter la nouvelle tentative manuelle : :

s.sendall(some_big_bytes)

9.13.2. Un serveur TCP

Le côté serveur se déroule en quatre étapes : revendiquer un port, basculer le socket en mode écoute, accepter les connexions une par une, dialoguer sur chaque socket accepté. Un serveur écho minimal : :

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 9000))
server.listen(1)
print("listening on port 9000")

while True:
    conn, addr = server.accept()
    print("connection from", addr)

    while True:
        data = conn.recv(1024)
        if not data:
            break
        conn.send(data)            # echo back

    conn.close()

Étape par étape :

  • bind() revendique un hôte et un port sur la caméra. "0.0.0.0" accepte sur n’importe quelle interface ; le remplacer par une adresse IP spécifique restreint l’écouteur à cette interface.

  • listen() fait passer le socket d’un socket normal à un socket en écoute. L’argument est le backlog – le nombre de connexions en attente que MicroPython mettra en file d’attente pendant que l’application est occupée. Choisissez un petit nombre ; 1 convient dans la plupart des cas.

  • accept() bloque jusqu’à ce qu’un client se connecte, puis retourne (conn, addr) : un nouveau socket représentant cette connexion unique, et l’adresse du client. Le socket en écoute lui-même reste ouvert pour en accepter d’autres.

  • Tous les octets de la conversation transitent par conn, le nouveau socket. Les lectures et écritures utilisent les mêmes appels recv() / send() que du côté client.

  • Lorsque le client se ferme, recv() retourne b"" ; la boucle interne se termine et le serveur ferme son extrémité avec close().

La boucle externe while True revient à accept() pour attendre le client suivant. Le serveur traite un client à la fois sous cette forme ; faire fonctionner plusieurs clients en parallèle nécessite soit des threads, soit asyncio. Ce dernier fait l’objet de la page suivante.

9.13.3. Pièges courants

  • Traiter recv() comme s’il avait la forme d’un message. Ce n’est pas le cas. Deux appels send(b"hi") peuvent arriver sous la forme d’un seul recv(4) de b"hihi", ou sous la forme de deux recv(2). L’application doit ajouter un découpage en trames si les limites des messages importent – un saut de ligne, un préfixe de longueur, peu importe.

  • Oublier de réessayer lors d’envois partiels. Utilisez sendall() pour tout ce qui dépasse quelques centaines d’octets.

  • Oublier de fermer le socket accepté. Chaque conn est un socket distinct ; fermer le socket en écoute ne ferme pas les sockets acceptés. Des blocs with sur les deux rendent cette erreur difficile à commettre : :

    while True:
        with server.accept()[0] as conn:
            # ... talk on conn ...
    
  • Se relier à un port encore en TIME_WAIT. Lorsqu’un serveur redémarre quelques secondes après sa fermeture, bind() peut échouer avec « address in use » parce que MicroPython retient encore le port pour la connexion précédente. server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) avant bind() corrige ce problème.

9.13.4. La suite

Bloquer sur accept() signifie que le serveur ne peut servir qu’un seul client à la fois. Bloquer sur recv() signifie qu’un seul client lent fige toute la boucle. La réponse standard sur la caméra est asyncio – exécuter chaque connexion comme sa propre tâche, et laisser la boucle d’événements répartir le temps entre elles. La page suivante couvre les versions asyncio de tout ce qui figure sur celle-ci.