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.