9.13. TCP sokety

TCP sokety se vyskytují ve dvou podobách, které vypadají odlišně, ale sdílejí stejný základní typ: klientské sokety, které se pomocí connect() připojí ke vzdálenému serveru, a serverové sokety, které pomocí bind(), listen() a accept() přijímají příchozí spojení. Obě role používají stejnou třídu socket představenou na stránce Objekty socketů; liší se pouze metody, které se na nich volají.

9.13.1. TCP klient

Nejjednodušší klient otevře spojení, odešle požadavek, přečte odpověď a zavře:

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() provede třícestný handshake popsaný na TCP – spolehlivý proud bajtů a vrátí řízení, jakmile je spojení otevřené. send() zapíše bajty do spojení; recv() z něj přečte až daný počet bajtů. Jakmile aplikace skončí, close() spojení ukončí.

Stejný skript zabalený do idiomu příkazu with ze stránky Objekty socketů, takže se soket zavře i v případě, že něco vyvolá výjimku:

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. Čtení až do konce

Jedno volání recv() vrátí požadovaný počet bajtů – může jich vrátit méně, protože TCP je proud (stream), nikoli posloupnost zpráv. Aplikace musí číst opakovaně, dokud nemá celou odpověď:

chunks = []
while True:
    chunk = s.recv(1024)
    if not chunk:                  # empty bytes -> other side closed
        break
    chunks.append(chunk)
reply = b"".join(chunks)

Smyčka skončí, když recv() vrátí prázdné bytes. K tomu dojde, když druhá strana čistě uzavřela svou polovinu spojení; aplikace v tomto stylu protokolu čte „konec proudu“ stejně jako „konec zprávy“.

9.13.1.2. Odesílání až do konce

Opačná výhrada platí pro send(): může odeslat méně bajtů, než bylo požadováno, a vrátí počet skutečně zapsaných bajtů. U velkých dat opakujte odeslání neodeslaného zbytku:

payload = some_big_bytes
while payload:
    n = s.send(payload)
    payload = payload[n:]

sendall() provádí tuto smyčku interně, takže většina kódu může jednoduše zavolat tuto metodu a vyhnout se ručnímu opakování:

s.sendall(some_big_bytes)

9.13.2. TCP server

Serverová strana má čtyři kroky: obsadit port, přepnout soket do režimu naslouchání, přijímat spojení jedno po druhém a komunikovat na každém přijatém soketu. Minimální echo server:

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()

Krok za krokem:

  • bind() obsadí hostitele a port na kameře. "0.0.0.0" přijímá na libovolném rozhraní; jeho nahrazením konkrétní IP adresou se naslouchání omezí pouze na dané rozhraní.

  • listen() přepne soket z normálního soketu na naslouchající soket. Argumentem je backlog – kolik čekajících spojení bude MicroPython řadit do fronty, zatímco je aplikace zaneprázdněna. Zvolte malé číslo; 1 je pro většinu případů dostačující.

  • accept() blokuje, dokud se nepřipojí klient, a poté vrátí (conn, addr): nový soket reprezentující toto jedno spojení a adresu klienta. Samotný naslouchající soket zůstává otevřený, aby mohl přijímat další.

  • Všechny bajty této konverzace procházejí přes conn, nový soket. Čtení a zápisy používají stejná volání recv() / send() jako na straně klienta.

  • Když klient ukončí spojení, recv() vrátí b""; vnitřní smyčka skončí a server zavře svou stranu pomocí close().

Vnější while True se vrátí zpět k accept(), aby čekal na dalšího klienta. Server v této podobě obsluhuje jednoho klienta po druhém; provoz více klientů paralelně vyžaduje buď vlákna, nebo asyncio. To druhé je tématem následující stránky.

9.13.3. Časté chyby

  • Považování recv() za zprávově orientované. To není. Dvě volání send(b"hi") mohou dorazit jako jedno recv(4) s b"hihi", nebo jako dvě recv(2). Aplikace musí přidat rámcování, pokud na hranicích zpráv záleží – odřádkování, prefix délky, cokoli.

  • Zapomenutí na opakování při krátkých odesláních. Pro cokoli nad několik stovek bajtů použijte sendall().

  • Zapomenutí zavřít přijatý soket. Každý conn je samostatný soket; zavření naslouchajícího soketu nezavře ty přijaté. Bloky with na obou to znesnadňují pokazit:

    while True:
        with server.accept()[0] as conn:
            # ... talk on conn ...
    
  • Opětovné navázání na port, který je stále ve stavu TIME_WAIT. Když se server restartuje během několika sekund od svého ukončení, bind() může selhat s chybou „address in use“, protože MicroPython stále drží port z předchozího spojení. server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) před bind() to vyřeší.

9.13.4. Co dál

Blokování na accept() znamená, že server může obsluhovat pouze jednoho klienta najednou. Blokování na recv() znamená, že jediný pomalý klient zablokuje celou smyčku. Standardním řešením na kameře je asyncio – spustit každé spojení jako vlastní úlohu a nechat smyčku událostí přepínat mezi nimi. Následující stránka popisuje asyncio verze všeho, co je na této stránce.