9.13. Gniazda TCP#
Gniazda TCP występują w dwóch postaciach, które wyglądają różnie, ale opierają się na tym samym typie bazowym: gniazda klienta, które wykonują connect() do zdalnego serwera, oraz gniazda serwera, które wykonują bind(), listen() oraz accept() na połączeniach przychodzących. Obie role korzystają z tej samej klasy socket przedstawionej na stronie Obiekty gniazd; różnią się jedynie wywoływane na nich metody.
9.13.1. Klient TCP#
Najprostszy klient otwiera połączenie, wysyła żądanie, odczytuje odpowiedź i zamyka połączenie:
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() przeprowadza trójstopniowe nawiązanie połączenia (handshake) opisane na stronie TCP – niezawodny strumień bajtów i zwraca sterowanie, gdy połączenie jest otwarte. send() zapisuje bajty do połączenia; recv() odczytuje z niego do podanej liczby bajtów. Gdy aplikacja zakończy pracę, close() zamyka połączenie.
Ten sam skrypt zapisany przy użyciu idiomu z instrukcją with ze strony Obiekty gniazd, dzięki czemu gniazdo zostaje zamknięte nawet wtedy, gdy coś zgłosi wyjątek:
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. Czytanie aż do końca#
Pojedyncze wywołanie recv() zwraca do żądanej liczby bajtów – może zwrócić ich mniej, ponieważ TCP jest strumieniem, a nie sekwencją komunikatów. Aplikacja musi czytać dalej, dopóki nie otrzyma pełnej odpowiedzi:
chunks = []
while True:
chunk = s.recv(1024)
if not chunk: # empty bytes -> other side closed
break
chunks.append(chunk)
reply = b"".join(chunks)
Pętla kończy się, gdy recv() zwróci pusty obiekt bytes. Dzieje się tak, gdy druga strona poprawnie zamknęła swoją połowę połączenia; w tym stylu protokołu aplikacja traktuje „koniec strumienia” tak samo jak „koniec komunikatu”.
9.13.1.2. Wysyłanie aż do końca#
Odwrotne zastrzeżenie dotyczy send(): może wysłać mniej bajtów niż żądano, zwracając liczbę faktycznie zapisanych bajtów. W przypadku dużych ładunków należy ponownie wysłać niewysłaną resztę:
payload = some_big_bytes
while payload:
n = s.send(payload)
payload = payload[n:]
sendall() wykonuje tę pętlę wewnętrznie, więc w większości przypadków kod może po prostu wywołać tę metodę i uniknąć ręcznego ponawiania:
s.sendall(some_big_bytes)
9.13.2. Serwer TCP#
Strona serwera składa się z czterech kroków: zajęcie portu, przełączenie gniazda w tryb nasłuchiwania, akceptowanie połączeń jedno po drugim oraz komunikacja na każdym zaakceptowanym gnieździe. Minimalny serwer echo:
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 po kroku:
bind()zajmuje hosta i port na kamerze."0.0.0.0"akceptuje połączenia na dowolnym interfejsie; zastąpienie tego konkretnym adresem IP ogranicza nasłuchiwanie do tego interfejsu.listen()przełącza gniazdo ze zwykłego gniazda na gniazdo nasłuchujące. Argument to backlog – liczba oczekujących połączeń, które MicroPython będzie kolejkować, gdy aplikacja jest zajęta. Wybierz małą wartość;1jest odpowiednie w większości przypadków.accept()blokuje wykonanie, dopóki klient się nie połączy, a następnie zwraca(conn, addr): nowe gniazdo reprezentujące to jedno połączenie oraz adres klienta. Samo gniazdo nasłuchujące pozostaje otwarte, aby przyjmować kolejne połączenia.Wszystkie bajty tej konwersacji przepływają przez
conn, nowe gniazdo. Odczyt i zapis korzystają z tych samych wywołańrecv()/send()co po stronie klienta.Gdy klient zamknie połączenie,
recv()zwracab""; wewnętrzna pętla się kończy, a serwer zamyka swoją stronę za pomocąclose().
Zewnętrzna pętla while True wraca do accept(), aby czekać na kolejnego klienta. W tej postaci serwer obsługuje jednego klienta naraz; obsługa wielu klientów równolegle wymaga albo wątków, albo asyncio. To ostatnie jest tematem następnej strony.
9.13.3. Częste pułapki#
Traktowanie recv() jak komunikatu. Tak nie jest. Dwa wywołania
send(b"hi")mogą dotrzeć jako jednorecv(4)zwracająceb"hihi"lub jako dwa wywołaniarecv(2). Aplikacja musi dodać ramkowanie, jeśli granice komunikatów mają znaczenie – znak nowego wiersza, prefiks z długością, cokolwiek.Zapominanie o ponawianiu przy krótkich wysyłkach. Dla czegokolwiek przekraczającego kilkaset bajtów używaj
sendall().Zapominanie o zamknięciu zaakceptowanego gniazda. Każde
connjest osobnym gniazdem; zamknięcie gniazda nasłuchującego nie zamyka gniazd zaakceptowanych. Blokiwithna obu utrudniają popełnienie tego błędu:while True: with server.accept()[0] as conn: # ... talk on conn ...
Ponowne wiązanie z portem wciąż w stanie TIME_WAIT. Gdy serwer uruchamia się ponownie w ciągu kilku sekund od zamknięcia,
bind()może zakończyć się błędem „address in use”, ponieważ MicroPython wciąż utrzymuje port dla poprzedniego połączenia. Wywołanieserver.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)przedbind()rozwiązuje ten problem.
9.13.4. Co dalej#
Blokowanie na accept() oznacza, że serwer może obsługiwać tylko jednego klienta naraz. Blokowanie na recv() oznacza, że pojedynczy wolny klient zawiesza całą pętlę. Standardowym rozwiązaniem na kamerze jest asyncio – uruchom każde połączenie jako własne zadanie i pozwól pętli zdarzeń przełączać się między nimi. Następna strona omawia wersje asyncio wszystkiego, co znajduje się na tej stronie.