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.