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; az 1 a 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 conn nevű új socketen keresztül áramlik. Az olvasás és írás ugyanazokat a recv() / send() hívásokat használja, mint a kliensoldalon.

  • Amikor a kliens lezárja a kapcsolatot, a recv() a b"" értéket adja vissza; a belső ciklus véget ér, és a kiszolgáló a close() 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 egyetlen recv(4) hívásként b"hihi" formában, vagy két recv(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 conn egy külön socket; a figyelő socket lezárása nem zárja le az elfogadott socketeket. Mindkettőn alkalmazott with blokk 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. A bind() előtt kiadott server.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.