9.13. TCP socketek¶
A TCP socketek két formában léteznek, amelyek különbözőnek tűnnek, de ugyanazon a közös alaptípuson osztoznak: kliens socketek, amelyek connect() hívással csatlakoznak egy távoli kiszolgálóhoz, és kiszolgáló socketek, amelyek bind(), listen() és accept() hívásokkal kezelik a bejövő kapcsolatokat. Mindkét szerep ugyanazt a socket osztályt használja, amelyet a Socket objektumok oldalon mutattunk be; csak a rajtuk meghívott metódusok különböznek.
9.13.1. TCP kliens¶
A legegyszerűbb kliens megnyit egy kapcsolatot, elküld egy kérést, beolvassa a választ, majd lezárja a kapcsolatot:
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()
A connect() lefuttatja a TCP – megbízható bájtfolyam oldalon tárgyalt háromutas kézfogást, és akkor tér vissza, amikor a kapcsolat megnyílt. A send() bájtokat ír a kapcsolatba; a recv() egy adott számú bájtig olvas belőle. Ha az alkalmazás végzett, a close() lezárja a kapcsolatot.
Ugyanaz a szkript a Socket objektumok oldalon bemutatott with-utasítás formájába csomagolva, így a socket akkor is lezárul, ha valami kivételt vált ki:
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. Olvasás a végéig¶
Egyetlen recv() hívás legfeljebb a kért számú bájtot adja vissza – adhat kevesebbet is, mert a TCP egy folyam, nem pedig üzenetek sorozata. Az alkalmazásnak addig kell olvasnia, amíg meg nem kapja a teljes választ:
chunks = []
while True:
chunk = s.recv(1024)
if not chunk: # empty bytes -> other side closed
break
chunks.append(chunk)
reply = b"".join(chunks)
A ciklus akkor ér véget, amikor a recv() üres bytes objektumot ad vissza. Ez akkor történik, amikor a másik fél szabályosan lezárta a kapcsolat saját felét; az alkalmazás a „folyam vége” állapotot ebben a protokollstílusban ugyanúgy értelmezi, mint az „üzenet vége” állapotot.
9.13.1.2. Küldés a végéig¶
Az ellenkező figyelmeztetés vonatkozik a send() hívásra: az a kértnél kevesebb bájtot is küldhet, és a ténylegesen kiírt bájtok számával tér vissza. Nagy hasznos teher esetén az el nem küldött maradékot újra meg kell próbálni:
payload = some_big_bytes
while payload:
n = s.send(payload)
payload = payload[n:]
A sendall() belsőleg végzi el ezt a ciklust, így a legtöbb kód egyszerűen ezt hívhatja, elkerülve a kézi újrapróbálkozást:
s.sendall(some_big_bytes)
9.13.2. TCP kiszolgáló¶
A kiszolgálói oldal négy lépésből áll: lefoglal egy portot, a socketet figyelő módba kapcsolja, egyenként fogadja a kapcsolatokat, és minden elfogadott socketen kommunikál. Egy minimális echo-kiszolgáló:
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()
Lépésről lépésre:
A
bind()lefoglal egy hostot és egy portot a kamerán. A"0.0.0.0"minden hálózati interfészen fogad; egy konkrét IP-re cserélve a figyelést arra az interfészre korlátozza.A
listen()egy normál socketről figyelő socketre kapcsolja a socketet. Az argumentum a backlog – hány függőben lévő kapcsolatot soroljon várólistára a MicroPython, amíg az alkalmazás el van foglalva. Válassz kis számot; az1a legtöbb esetben megfelel.A
accept()blokkol, amíg egy kliens nem csatlakozik, majd visszaadja a(conn, addr)párost: egy új socketet, amely ezt az egy kapcsolatot képviseli, valamint a kliens címét. Maga a figyelő socket nyitva marad, hogy további kapcsolatokat fogadhasson.A beszélgetés összes bájtja a
connnevű új socketen keresztül áramlik. Az olvasás és írás ugyanazokat arecv()/send()hívásokat használja, mint a kliensoldalon.Amikor a kliens lezárja a kapcsolatot, a
recv()ab""értéket adja vissza; a belső ciklus véget ér, és a kiszolgáló aclose()hívással lezárja a maga oldalát.
A külső while True visszaugrik a accept() híváshoz, hogy megvárja a következő klienst. A kiszolgáló ebben a formában egyszerre egy klienst kezel; több kliens párhuzamos futtatásához vagy szálakra, vagy az asyncio modulra van szükség. Ez utóbbi a következő oldal témája.
9.13.3. Gyakori buktatók¶
A recv() üzenetformájúként kezelése. Nem az. Két
send(b"hi")hívás megérkezhet egyetlenrecv(4)híváskéntb"hihi"formában, vagy kétrecv(2)ként. Az alkalmazásnak kereteket kell hozzáadnia, ha az üzenethatárok számítanak – egy újsor-karaktert, egy hosszelőtagot, vagy bármi mást.Az újrapróbálkozás elmulasztása rövid küldéseknél. Használd a
sendall()hívást bármi olyanra, ami néhány száz bájtnál nagyobb.Az elfogadott socket lezárásának elmulasztása. Minden
connegy külön socket; a figyelő socket lezárása nem zárja le az elfogadott socketeket. Mindkettőn alkalmazottwithblokk megnehezíti, hogy ezt elronthassuk:while True: with server.accept()[0] as conn: # ... talk on conn ...
Újrakötés egy még TIME_WAIT állapotban lévő porthoz. Ha egy kiszolgáló a lezárást követő néhány másodpercen belül újraindul, a
bind()„address in use” hibával hiúsulhat meg, mert a MicroPython még tartja a portot az előző kapcsolat számára. Abind()előtt kiadottserver.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)ezt megszünteti.
9.13.4. Mi következik¶
A accept() hívásnál való blokkolás azt jelenti, hogy a kiszolgáló egyszerre csak egy klienst tud kiszolgálni. A recv() hívásnál való blokkolás azt jelenti, hogy egyetlen lassú kliens megakasztja az egész ciklust. A kamerán a szokásos válasz az asyncio – minden kapcsolatot saját feladatként futtatva, hagyva, hogy az eseményhurok váltogasson közöttük. A következő oldal ennek az oldalnak minden elemét bemutatja asyncio-változatban.