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; 1 je 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 pozive recv() / send() kao na klijentskoj strani.

  • Kada klijent zatvori vezu, recv() vraća b""; unutarnja petlja završava i poslužitelj zatvara svoj kraj pozivom close().

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 jedan recv(4) s vrijednošću b"hihi", ili kao dva recv(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 conn je zasebna utičnica; zatvaranje osluškivačke utičnice ne zatvara prihvaćene. Blokovi with na 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) prije bind() 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.