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.