9.13. Socket TCP

I socket TCP esistono in due forme che sembrano diverse ma condividono lo stesso tipo sottostante: i socket client che si connect() a un server remoto, e i socket server che fanno bind(), listen() e accept() delle connessioni in arrivo. Entrambi i ruoli usano la stessa classe socket introdotta in Oggetti socket; differiscono solo i metodi chiamati su di essi.

9.13.1. Un client TCP

Il client più semplice apre una connessione, invia una richiesta, legge la risposta e chiude:

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() esegue l’handshake a tre vie descritto in TCP – un flusso affidabile di byte e ritorna quando la connessione è aperta. send() scrive byte sulla connessione; recv() legge fino a un dato numero di byte da essa. Una volta che l’applicazione ha finito, close() chiude la connessione.

Lo stesso script avvolto nell’idioma dell’istruzione with visto in Oggetti socket, così che il socket venga chiuso anche se qualcosa solleva un’eccezione:

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. Leggere fino al completamento

Una singola recv() restituisce fino a il numero di byte richiesto – può restituirne meno, perché il TCP è un flusso piuttosto che una sequenza di messaggi. L’applicazione deve continuare a leggere finché non ha la risposta completa:

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

Il ciclo termina quando recv() restituisce un oggetto bytes vuoto. Ciò avviene quando l’altra parte ha chiuso in modo pulito la sua metà della connessione; in questo stile di protocollo l’applicazione interpreta la «fine del flusso» come equivalente alla «fine del messaggio».

9.13.1.2. Inviare fino al completamento

L’avvertenza opposta vale per send(): può inviare meno byte di quelli richiesti, restituendo il conteggio dei byte effettivamente scritti. Per payload di grandi dimensioni, ritentare l’invio della parte rimanente non inviata:

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

sendall() esegue il ciclo internamente, così che la maggior parte del codice possa semplicemente chiamarlo ed evitare il tentativo manuale:

s.sendall(some_big_bytes)

9.13.2. Un server TCP

Il lato server consiste in quattro passi: rivendicare una porta, mettere il socket in modalità di ascolto, accettare le connessioni una alla volta, dialogare su ciascun socket accettato. Un server di echo minimale:

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()

Passo dopo passo:

  • bind() rivendica un host e una porta sulla camera. "0.0.0.0" accetta su qualsiasi interfaccia; sostituendolo con un IP specifico si limita l’ascoltatore a quella interfaccia.

  • listen() trasforma il socket da un socket normale in un socket in ascolto. L’argomento è il backlog – quante connessioni in attesa MicroPython metterà in coda mentre l’applicazione è occupata. Scegliere un numero piccolo; 1 va bene nella maggior parte dei casi.

  • accept() blocca finché un client non si connette, poi restituisce (conn, addr): un nuovo socket che rappresenta questa singola connessione, e l’indirizzo del client. Il socket in ascolto rimane aperto per accettarne altre.

  • Tutti i byte della conversazione transitano attraverso conn, il nuovo socket. Le letture e le scritture usano le stesse chiamate recv() / send() del lato client.

  • Quando il client chiude, recv() restituisce b""; il ciclo interno termina e il server chiude la sua estremità con close().

Il while True esterno torna a accept() per attendere il client successivo. Con questa struttura il server gestisce un client alla volta; eseguire più client in parallelo richiede thread oppure asyncio. Quest’ultimo è l’argomento della pagina successiva.

9.13.3. Insidie comuni

  • Trattare recv() come se fosse orientato ai messaggi. Non lo è. Due chiamate send(b"hi") potrebbero arrivare come una sola recv(4) di b"hihi", oppure come due recv(2). L’applicazione deve aggiungere un framing se i confini dei messaggi sono importanti – un a capo, un prefisso di lunghezza, qualsiasi cosa.

  • Dimenticare di ritentare in caso di invii parziali. Usare sendall() per qualsiasi cosa oltre poche centinaia di byte.

  • Dimenticare di chiudere il socket accettato. Ogni conn è un socket separato; chiudere il socket in ascolto non chiude quelli accettati. I blocchi with su entrambi rendono difficile sbagliare:

    while True:
        with server.accept()[0] as conn:
            # ... talk on conn ...
    
  • Ri-effettuare il bind su una porta ancora in TIME_WAIT. Quando un server viene riavviato pochi secondi dopo la chiusura, bind() può fallire con «address in use» perché MicroPython sta ancora tenendo occupata la porta per la connessione precedente. server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) prima di bind() risolve il problema.

9.13.4. Cosa viene dopo

Bloccarsi su accept() significa che il server può servire un solo client alla volta. Bloccarsi su recv() significa che un singolo client lento blocca l’intero ciclo. La risposta standard sulla camera è asyncio – eseguire ogni connessione come un proprio task e lasciare che il ciclo di eventi alterni tra di essi. La pagina successiva tratta le versioni asyncio di tutto ciò che è presente in questa.