9.13. Socket-uri TCP¶
Socket-urile TCP apar în două forme care arată diferit, dar împart același tip de bază: socket-uri client care folosesc connect() pentru a se conecta la un server la distanță, și socket-uri server care folosesc bind(), listen() și accept() pentru a accepta conexiunile primite. Ambele roluri folosesc aceeași clasă socket prezentată în Obiecte socket; diferă doar metodele apelate pe ele.
9.13.1. Un client TCP¶
Cel mai simplu client deschide o conexiune, trimite o cerere, citește răspunsul și se închide:
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() execută strângerea de mână în trei pași descrisă în TCP – un flux fiabil de octeți și revine atunci când conexiunea este deschisă. send() scrie octeți în conexiune; recv() citește până la un număr dat de octeți din ea. După ce aplicația a terminat, close() închide conexiunea.
Același script încadrat în formula idiomatică cu instrucțiunea with din Obiecte socket, astfel încât socket-ul este închis chiar dacă ceva generează o excepție:
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. Citirea până la finalizare¶
Un singur apel recv() returnează cel mult numărul de octeți cerut – poate returna mai puțini, deoarece TCP este un flux, nu o secvență de mesaje. Aplicația trebuie să continue să citească până când are răspunsul complet:
chunks = []
while True:
chunk = s.recv(1024)
if not chunk: # empty bytes -> other side closed
break
chunks.append(chunk)
reply = b"".join(chunks)
Bucla se termină când recv() returnează un obiect bytes gol. Acest lucru se întâmplă atunci când cealaltă parte și-a închis curat jumătatea sa de conexiune; aplicația interpretează „sfârșitul fluxului” ca fiind același lucru cu „sfârșitul mesajului” în acest stil de protocol.
9.13.1.2. Trimiterea până la finalizare¶
Avertismentul opus se aplică pentru send(): poate trimite mai puțini octeți decât s-a cerut, returnând numărul de octeți efectiv scriși. Pentru sarcini utile mari, reîncercați partea netrimisă rămasă:
payload = some_big_bytes
while payload:
n = s.send(payload)
payload = payload[n:]
sendall() execută bucla intern, astfel încât majoritatea codului poate apela pur și simplu această metodă și poate evita reîncercarea manuală:
s.sendall(some_big_bytes)
9.13.2. Un server TCP¶
Partea de server constă în patru pași: rezervarea unui port, comutarea socket-ului în modul de ascultare, acceptarea conexiunilor una câte una și comunicarea pe fiecare socket acceptat. Un server echo 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()
Pas cu pas:
bind()rezervă o gazdă și un port pe cameră."0.0.0.0"acceptă pe orice interfață; înlocuirea sa cu o adresă IP specifică restricționează ascultătorul la acea interfață.listen()comută socket-ul de la un socket normal la un socket de ascultare. Argumentul este backlog-ul – câte conexiuni în așteptare va pune MicroPython în coadă cât timp aplicația este ocupată. Alegeți un număr mic;1este suficient în majoritatea cazurilor.accept()blochează până când un client se conectează, apoi returnează(conn, addr): un socket nou care reprezintă această conexiune anume și adresa clientului. Socket-ul de ascultare în sine rămâne deschis pentru a accepta alte conexiuni.Toți octeții conversației circulă prin
conn, noul socket. Citirile și scrierile folosesc aceleași apelurirecv()/send()ca pe partea de client.Când clientul se închide,
recv()returneazăb""; bucla interioară se termină, iar serverul își închide capătul cuclose().
Bucla exterioară while True revine la accept() pentru a aștepta următorul client. În această formă, serverul gestionează câte un client pe rând; rularea mai multor clienți în paralel necesită fie fire de execuție, fie asyncio. Acesta din urmă este subiectul paginii următoare.
9.13.3. Capcane frecvente¶
Tratarea recv() ca și cum ar avea formă de mesaj. Nu este așa. Două apeluri
send(b"hi")ar putea sosi ca un singurrecv(4)deb"hihi", sau ca douărecv(2)-uri. Aplicația trebuie să adauge încadrare dacă limitele mesajelor contează – o linie nouă, un prefix de lungime, orice.Uitarea reîncercării la trimiteri scurte. Folosiți
sendall()pentru orice depășește câteva sute de octeți.Uitarea închiderii socket-ului acceptat. Fiecare
conneste un socket separat; închiderea socket-ului de ascultare nu le închide pe cele acceptate. Blocurilewithpe ambele fac dificilă greșeala:while True: with server.accept()[0] as conn: # ... talk on conn ...
Re-asocierea la un port aflat încă în TIME_WAIT. Când un server repornește la câteva secunde după închidere,
bind()poate eșua cu „address in use”, deoarece MicroPython încă deține portul pentru conexiunea anterioară.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)înainte debind()rezolvă acest lucru.
9.13.4. Ce urmează¶
Blocarea pe accept() înseamnă că serverul poate deservi un singur client la un moment dat. Blocarea pe recv() înseamnă că un singur client lent blochează întreaga buclă. Răspunsul standard pe cameră este asyncio – rulați fiecare conexiune ca propria sarcină și lăsați bucla de evenimente să distribuie între ele. Pagina următoare acoperă versiunile asyncio ale tuturor lucrurilor de pe această pagină.