9.13. TCP utičnice¶
TCP utičnice dolaze u dva oblika koji izgledaju različito, ali dijele isti temeljni tip: klijentske utičnice koje se metodom connect() spajaju na udaljeni poslužitelj te poslužiteljske utičnice koje pozivaju bind(), listen() i accept() za dolazne veze. Obje uloge koriste istu klasu socket predstavljenu na Objekti utičnice; razlikuju se samo metode koje se na njima pozivaju.
9.13.1. TCP klijent¶
Najjednostavniji klijent otvara vezu, šalje zahtjev, čita odgovor i zatvara vezu:
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() izvodi trostruko rukovanje opisano na TCP – pouzdan tok bajtova i vraća se kada je veza otvorena. send() zapisuje bajtove u vezu; recv() čita do zadanog broja bajtova iz nje. Kada aplikacija završi, close() zatvara vezu.
Ista skripta umotana u idiom naredbe with iz Objekti utičnice, tako da se utičnica zatvori čak i ako nešto izazove iznimku:
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. Čitanje do kraja¶
Jedan poziv recv() vraća najviše traženi broj bajtova – može vratiti i manje, jer je TCP tok, a ne niz poruka. Aplikacija mora nastaviti čitati dok ne dobije cijeli odgovor:
chunks = []
while True:
chunk = s.recv(1024)
if not chunk: # empty bytes -> other side closed
break
chunks.append(chunk)
reply = b"".join(chunks)
Petlja završava kada recv() vrati prazan bytes. To se događa kada je druga strana uredno zatvorila svoju polovicu veze; aplikacija čita „kraj toka” kao isto što i „kraj poruke” u ovom stilu protokola.
9.13.1.2. Slanje do kraja¶
Suprotno upozorenje vrijedi za send(): ona može poslati manje bajtova nego što je zatraženo, vraćajući broj bajtova koji su stvarno zapisani. Za velike sadržaje ponovno pošaljite neposlani ostatak:
payload = some_big_bytes
while payload:
n = s.send(payload)
payload = payload[n:]
sendall() interno izvodi petlju, pa većina koda može jednostavno pozvati to i izbjeći ručno ponavljanje:
s.sendall(some_big_bytes)
9.13.2. TCP poslužitelj¶
Poslužiteljska strana ima četiri koraka: zauzmi port, prebaci utičnicu u način osluškivanja, prihvaćaj veze jednu po jednu, razgovaraj na svakoj prihvaćenoj utičnici. Minimalni echo poslužitelj:
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()
Korak po korak:
bind()zauzima glavno računalo i port na kameri."0.0.0.0"prihvaća na bilo kojem sučelju; zamjena određenom IP adresom ograničava osluškivača na to sučelje.listen()prebacuje utičnicu iz obične utičnice u osluškivačku utičnicu. Argument je backlog – koliko će čekajućih veza MicroPython staviti u red dok je aplikacija zauzeta. Odaberite mali broj;1je u redu za većinu slučajeva.accept()blokira dok se klijent ne spoji, a zatim vraća(conn, addr): novu utičnicu koja predstavlja ovu jednu vezu i adresu klijenta. Sama osluškivačka utičnica ostaje otvorena za prihvaćanje daljnjih veza.Svi bajtovi razgovora prolaze kroz
conn, novu utičnicu. Čitanje i pisanje koriste iste poziverecv()/send()kao na klijentskoj strani.Kada klijent zatvori vezu,
recv()vraćab""; unutarnja petlja završava i poslužitelj zatvara svoj kraj pozivomclose().
Vanjska petlja while True skače natrag na accept() da pričeka sljedećeg klijenta. Poslužitelj u ovom obliku obrađuje jednog klijenta odjednom; pokretanje više klijenata paralelno zahtijeva ili dretve ili asyncio. Potonje je tema sljedeće stranice.
9.13.3. Česte zamke¶
Tretiranje recv() kao da ima oblik poruke. Nema ga. Dva poziva
send(b"hi")mogu stići kao jedanrecv(4)s vrijednošćub"hihi", ili kao dvarecv(2)-a. Aplikacija mora dodati uokvirivanje ako su granice poruka važne – novi redak, prefiks duljine, što god.Zaboravljanje ponavljanja pri kratkim slanjima. Koristite
sendall()za sve veće od nekoliko stotina bajtova.Zaboravljanje zatvaranja prihvaćene utičnice. Svaki
connje zasebna utičnica; zatvaranje osluškivačke utičnice ne zatvara prihvaćene. Blokoviwithna obje čine ovo teško pogrešnim:while True: with server.accept()[0] as conn: # ... talk on conn ...
Ponovno povezivanje na port koji je još u stanju TIME_WAIT. Kada se poslužitelj ponovno pokrene u roku od nekoliko sekundi nakon zatvaranja,
bind()može pasti s porukom „address in use” jer MicroPython još drži port za prethodnu vezu.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)prijebind()rješava to.
9.13.4. Što slijedi¶
Blokiranje na accept() znači da poslužitelj može posluživati samo jednog klijenta odjednom. Blokiranje na recv() znači da jedan spori klijent zaglavi cijelu petlju. Standardni odgovor na kameri je asyncio – pokrenite svaku vezu kao vlastiti zadatak, neka petlja događaja raspoređuje između njih. Sljedeća stranica pokriva asyncio inačice svega s ove stranice.