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í až 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;1je 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 jednorecv(4)sb"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ý
connje samostatný soket; zavření naslouchajícího soketu nezavře ty přijaté. Blokywithna 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ředbind()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.