Transport

Programowanie Socket

Interfejsy programowania sieciowego i API socket dla TCP i UDP.

Wprowadzenie: Interfejs Gniazd

TCP i UDP definiują usługi transportowe, lecz aplikacja potrzebuje interfejsu systemu operacyjnego, aby z nich korzystać. Przeglądarka, komunikator i inne aplikacje sieciowe używają Socket API do utworzenia punktu końcowego, wyboru protokołu transportowego oraz wymiany danych za pośrednictwem stosu sieciowego systemu.

Interfejs Programowania Aplikacji Gniazd (Socket API) to zestaw funkcji i struktur danych służących do tworzenia punktów końcowych, przypisywania adresów, ustanawiania połączeń oraz wysyłania i odbierania danych. Oddziela logikę aplikacji od wewnętrznego stanu i przetwarzania pakietów przez stos TCP/IP systemu operacyjnego.

Gniazdo (socket) jest punktem końcowym utrzymywanym przez system operacyjny i reprezentowanym w aplikacji przez deskryptor. Aplikacja przekazuje przez API parametry protokołu i adresy, system utrzymuje stan połączenia, wykonuje przetwarzanie transportowe oraz wybiera wymaganą ścieżkę sieciową.

Deskryptor identyfikuje jeden lokalny obiekt gniazda i jest przekazywany do kolejnych wywołań, takich jak 'bind()', 'send()' oraz 'close()'. Jego wartość liczbowa ma znaczenie tylko w procesie, który go posiada. Druga strona widzi adres oraz stan transportu, lecz nie ten deskryptor.

Zrozumienie Socket API

definiuje wywołania i kontrakty danych, za pomocą których komponenty oprogramowania współpracują. Socket API udostępnia funkcje takie jak 'socket()', 'connect()', 'send()' i 'recv()', więc program nie musi sam implementować uzgadniania TCP, obliczania sum kontrolnych ani routingu IP.

Berkeley Sockets API wprowadzono w systemie UNIX w ramach Berkeley Software Distribution (BSD). Interfejs został szeroko przyjęty w programowaniu sieciowym. Implementacje są dostępne między innymi w systemach Windows, Linux, macOS, iOS i Android, przy czym szczegóły oraz obsługiwane opcje zależą od platformy.

Typowe wywołanie 'socket()' wybiera rodzinę adresów, taką jak 'AF_INET', typ, na przykład 'SOCK_STREAM' lub 'SOCK_DGRAM', oraz wartość protokołu. Zwrócony deskryptor trzeba sprawdzić pod kątem błędu, ponieważ limity zasobów albo nieobsługiwana kombinacja parametrów mogą uniemożliwić utworzenie gniazda.

Czym Jest Gniazdo? Model Deskryptora Pliku

Gniazdo to podstawowy obiekt tworzony przez aplikację do komunikacji przez sieć. W systemach używających deskryptorów plików system operacyjny udostępnia je aplikacji jako deskryptor, który można wykorzystać w operacjach wejścia i wyjścia. Podstawowe operacje to:

  • Otworzyć plik, aby zacząć z niego czytać lub do niego pisać.
  • Czytać z pliku, aby otrzymać dane.
  • Pisać do pliku, aby wysłać dane.
  • Zamknąć plik, gdy się z nim skończyło.

W programowaniu gniazd odpowiadają im następujące operacje:

  • Tworzy się gniazdo, aby uzyskać "uchwyt" do komunikacji.
  • Czyta z gniazda (lub odbiera na nim), aby pobrać dane z sieci.
  • Pisze do gniazda (lub wysyła na nim), aby przesłać dane przez sieć.
  • Zamyka się gniazdo, aby zakończyć połączenie.

Deskryptor można również zarejestrować w mechanizmie gotowości, takim jak 'select()', 'poll()' albo systemowa funkcja zdarzeń. Mechanizm informuje, że operacja może się rozpocząć, lecz kolejne odczytanie lub zapis nadal może przenieść mniej bajtów niż żądano i wymaga sprawdzenia wyniku.

Dwa Główne Typy Gniazd

Kiedy programista tworzy gniazdo, musi wybrać jego typ, co bezpośrednio odpowiada protokołowi transportowemu, którego zamierza użyć. Dwa najczęstsze typy to gniazda strumieniowe i gniazda datagramowe.

Gniazda Strumieniowe (TCP)

Typ: 'SOCK_STREAM'

Te gniazda używają protokołu TCP i zapewniają niezawodny, zorientowany połączeniowo, dwukierunkowy, uporządkowany strumień bajtów. TCP nie zachowuje granic wiadomości. Jeśli aplikacja zapisze 10 bajtów, a następnie 20 bajtów, wywołanie odbioru może zwrócić łącznie 30 bajtów, tylko ich część albo dane z obu zapisów w innym podziale.

Przykład: Protokół aplikacyjny działający nad TCP musi sam definiować ramkowanie, na przykład przez prefiks długości albo separator.

Pojedyncze wywołanie 'send()' lub 'write()' może także przyjąć tylko część przekazanego bufora. Nadawca powtarza operację do zapisania całej ramki, a odbiorca gromadzi bajty do chwili spełnienia reguły ramkowania.

Gniazda Datagramowe (UDP)

Typ: 'SOCK_DGRAM'

Te gniazda używają protokołu UDP i zapewniają zawodną, bezpołączeniową usługę datagramową zorientowaną na wiadomości. Granice datagramów są zachowane, jeśli bufor odbiorczy ma wystarczający rozmiar. Po wysłaniu datagramu 10-bajtowego, a następnie 20-bajtowego odbiorca otrzyma dwa datagramy o długości 10 i 20 bajtów.

Przykład: Każde wywołanie 'sendto()' przekazuje jeden datagram. Datagram może zaginąć lub dotrzeć w innej kolejności, więc wymaganą niezawodność obsługuje aplikacja.

Gdy bufor odbiorczy jest za mały, API może zwrócić tylko mieszczącą się część i zgłosić obcięcie zgodnie z zasadami platformy. Aplikacja musi dobrać bufor do maksymalnego rozmiaru datagramu wymaganego przez protokół, jeśli chce zachować wszystkie bajty.

Cykl Życia Gniazda TCP

Cykl życia gniazda TCP obejmuje inne wywołania po stronie serwera i klienta. Serwer przygotowuje pasywne gniazdo nasłuchujące, a klient inicjuje połączenie.

Przepływ Pracy po Stronie Serwera TCP

  1. 'socket()' - Utwórz Gniazdo:Pierwszym krokiem serwera jest poproszenie systemu operacyjnego o utworzenie punktu końcowego gniazda. Programista określa rodzinę adresów (np. IPv4) i typ gniazda ('SOCK_STREAM' dla TCP). System operacyjny zwraca deskryptor pliku, małą liczbę całkowitą, która działa jak identyfikator dla tego nowego gniazda.
    Wynik: Deskryptor identyfikuje ten punkt końcowy w kolejnych wywołaniach.
    Typowe argumenty:Serwer TCP dla IPv4 zwykle wybiera 'AF_INET', 'SOCK_STREAM' i protokół 0, aby system dobrał TCP dla wskazanego typu.
  2. 'bind()' - Przypisz Adres:Nowe gniazdo to tylko ogólny punkt końcowy. Aby było użyteczne, serwer musi powiązać je z konkretnym adresem IP i numerem portu na maszynie. Funkcja 'bind()' to robi. Serwer WWW powiązałby swoje gniazdo z publicznym adresem IP serwera i dobrze znanym portem 80.
    Przykład: Serwer WWW może powiązać gniazdo nasłuchujące z lokalnym adresem i dobrze znanym portem 80.
    Adres lokalny typu wildcard może oznaczać odbiór na kwalifikowanych interfejsach hosta, natomiast wskazanie konkretnego adresu ogranicza punkt końcowy do danego interfejsu.
  3. 'listen()' - Przejdź do Stanu Nasłuchiwania:Powiązanie gniazda przypisuje adres lokalny, lecz nie czyni go gniazdem nasłuchującym. Funkcja 'listen()' przełącza gniazdo strumieniowe w tryb pasywny. Parametr 'backlog' określa limit oczekujących połączeń. Aplikacja może zażądać kolejki długości 5, z uwzględnieniem reguł systemu operacyjnego.
    Kolejka backlog obejmuje połączenia oczekujące na odebranie przez aplikację. Nie rezerwuje przepustowości i nie określa liczby ustanowionych gniazd, które proces może utrzymywać.
  4. 'accept()' - Zwróć Połączenie: W przypadku gniazda blokującego 'accept()' czeka na zakończone połączenie w kolejce oczekujących po ukończeniu przez system trójetapowego uzgadniania TCP. Zwraca nowe gniazdo przeznaczone dla danego klienta oraz jego deskryptor. Oryginalne gniazdo nasłuchujące pozostaje w stanie LISTEN i może przyjmować kolejne połączenia.
  5. 'read()'/'recv()' i 'write()'/'send()' - Komunikuj się: Serwer może teraz używać nowego gniazda połączenia do komunikacji z klientem, odczytując żądania i zapisując odpowiedzi, tak jakby pisał i czytał z pliku.
    TCP jest strumieniem bajtów, dlatego wywołania mogą zakończyć się częściowo albo zwrócić zero po uporządkowanym zamknięciu przez partnera. Aplikacja musi obsłużyć zwróconą liczbę i określić moment odebrania pełnego żądania lub odpowiedzi.
  6. 'close()' - Zakończ Połączenie:Po zakończeniu wymiany serwer wywołuje 'close()' na połączonym gnieździe. Przy uporządkowanym zamknięciu TCP rozpoczyna to wymianę FIN, zwykle opisywaną jako zamknięcie czteroetapowe. Gniazdo nasłuchujące zamyka się osobno podczas zatrzymywania serwera.

Przepływ Pracy po Stronie Klienta TCP

Klient tworzy punkt końcowy i aktywnie inicjuje połączenie.

  1. 'socket()' - Utwórz Gniazdo:Tak jak serwer, klient musi najpierw utworzyć punkt końcowy gniazda ('SOCK_STREAM' dla TCP).
  2. 'connect()' - Nawiąż Połączenie:Zamiast wiązania i nasłuchiwania klient wywołuje 'connect()' z adresem IP serwera i numerem portu. System wykonuje trójetapowe uzgadnianie TCP. Wywołanie blokujące czeka na stan 'ESTABLISHED' albo błąd, natomiast wywołanie nieblokujące zgłasza postęp przez mechanizmy gotowości i błędów. System zwykle automatycznie przypisuje efemeryczny port, jeśli klient nie powiązał własnego.
    Przepływ TCP identyfikuje pełna krotka lokalnego i zdalnego adresu oraz portu wraz z protokołem. Jeden deskryptor może obsługiwać serię uporządkowanych żądań zgodnie z protokołem aplikacyjnym i stanem połączenia.
  3. 'write()'/'send()' i 'read()'/'recv()' - Komunikuj się: Po nawiązaniu połączenia klient może zacząć wysyłać żądania do serwera i odbierać odpowiedzi przez swoje gniazdo.
  4. 'close()' - Zakończ Połączenie:Po odebraniu potrzebnych danych klient wywołuje 'close()' na swoim gnieździe i rozpoczyna swoją część uporządkowanej wymiany FIN TCP.

Cykl Życia Gniazda UDP: Model Bezpołączeniowy

Gniazda UDP nie wymagają uzgadniania ani trwałego skojarzenia transportowego. Każdy datagram jest niezależną jednostką, choć aplikacja może wywołać 'connect()', aby wybrać domyślnego partnera gniazda.

Wywołanie 'connect()' dla UDP nie tworzy sesji podobnej do TCP. Zapisuje domyślny adres docelowy i na wielu systemach może ograniczyć odbieranie do tego partnera, lecz dostarczanie, kolejność oraz retransmisja pozostają poza UDP.

Przepływ Pracy po Stronie Serwera UDP

  1. 'socket()' - Utwórz Gniazdo:Serwer tworzy gniazdo, ale tym razem określa typ jako 'SOCK_DGRAM' dla komunikacji datagramowej.
  2. 'bind()' - Przypisz Adres: Ten krok jest identyczny jak w TCP. Serwer UDP musi powiązać swoje gniazdo z dobrze znanym portem, aby klienci wiedzieli, gdzie wysyłać swoje datagramy.
  3. 'recvfrom()' - Czekaj na Datagram: Nie ma funkcji 'listen()' ani 'accept()'. Serwer UDP po prostu wywołuje 'recvfrom()'. Funkcja ta blokuje się, dopóki na powiązanym porcie nie pojawi się datagram. Po zakończeniu oczekiwania przekazuje odebrane dane oraz adres źródłowy (IP i port) klienta, który je wysłał.
    Wynik: Wywołanie zwraca datagram oraz adres źródłowy, dzięki czemu serwer może powiązać odpowiedź z nadawcą.
    Zwrócona długość opisuje datagram przekazany przez API. Serwer powinien sprawdzić tę długość oraz pola aplikacyjne przed przygotowaniem odpowiedzi.
  4. 'sendto()' - Wyślij Odpowiedź:Bez połączonego partnera UDP serwer używa 'sendto()' i podaje dane oraz adres docelowy uzyskany z 'recvfrom()'.

Przepływ Pracy po Stronie Klienta UDP

  1. 'socket()' - Utwórz Gniazdo:Klient tworzy gniazdo datagramowe ('SOCK_DGRAM').
  2. 'sendto()' - Wyślij Datagram:Klient przygotowuje dane i wywołuje 'sendto()' z adresem serwera (IP i port), bez uzgadniania. Może też wywołać 'connect()', aby ustawić domyślnego partnera, a następnie używać 'send()' lub 'write()'. System automatycznie przydzieli port efemeryczny, jeśli gniazdo nie zostało wcześniej powiązane.
  3. 'recvfrom()' - Czekaj na Odpowiedź:Klient następnie zazwyczaj wywołuje 'recvfrom()', aby czekać na datagram odpowiedzi od serwera na swoim porcie efemerycznym.
    Klient może porównać zwrócony adres źródłowy z oczekiwanym serwerem i odrzucić niepowiązaną odpowiedź, jeśli wymaga tego protokół.

Powiązane artykuły