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;1va 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 chiamaterecv()/send()del lato client.Quando il client chiude,
recv()restituisceb""; il ciclo interno termina e il server chiude la sua estremità conclose().
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 solarecv(4)dib"hihi", oppure come duerecv(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 blocchiwithsu 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 dibind()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.