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.