Kodujże!

blog krakoskiego programisty

java.nio – Jak zbudować prosty nieblokujący serwer?

Ostatnio zacząłem uważniej przyglądać się reaktywnemu programowaniu i frameworkowi WebFlux, który ukazał się wraz z releasem Spring 5.0. Przy tej okazji spędziłem sporo czasu na czytaniu o nieblokujących serwerach HTTP, będących swoistego rodzaju fundamentem, na którym zbudowany jest cały ten reaktywny stos. Fundamentem, który dla wielu programistów jest… czarną skrzynką. Być może to tylko moje odczucie, ale odnoszę wrażenie, że coraz częściej uczymy się API frameworków i bibliotek, a nie zasad ich działania. Z tego powodu postanowiłem napisać krótki post o pakiecie java.nio, w opraciu o który zbudowane są takie projekty jak Netty czy Undertow (używane we wspomnianym WebFluxie).

Wpisu tego nie można traktować jako kompendium wiedzy o pakiecie java.nio. Moim celem jest przedstawienie zagadnienia w sposób zwięzły i zrozumiały, a nie ślepe przepisywanie dokumentacji. 🙂

Miłej lektury!

Czym jest nieblokujący serwer?

W „tradycyjnym” podejściu serwer nasłuchuje w pętli ruch na zadanym porcie, a gdy tylko pojawia się nowe żądanie, oddelegowuje pracę związaną z jego obsługą do wcześniej utworzonej puli wątków. Model ten ma pewne wady. Po pierwsze, w każdym momencie liczba jednocześnie obsługiwanych klientów może być co najwyżej równa rozmiarowi puli. Po drugie, jeżeli chociaż jeden z tych klientów korzysta ze słabego łącza, to wątek, który został mu przypisany, przez większość czasu jest bezczynny w swoim oczekiwaniu na kolejne bity zapytania. W tym czasie mógłby zająć się innym klientem, więc jest to poważne marnowanie zasobów.

Nieblokujący serwer to serwer, który stara się zaadresować te problemy. W nieblokującym serwerze jeden wątek może obsługiwać wiele zapytań naraz. Jest to możliwe dzięki zastosowaniu nieblokującego IO, implementowanego w Javie przez klasy z pakietu java.nio.

java.nio

Wbrew  powszechnemu przekonaniu NIO nie jest akronimem od Non-blocking IO, lecz New IO (java.nio jest nowszą wersją paczki java.io, a nie tylko jej rozszerzeniem). Pakiet ten jest częścią biblioteki standardowej Javy od wersji 1.4 i dostarcza narzędzi służących do przeprowadzania operacji wejścia/wyjścia zarówno w sposób blokujący, jak i nieblokujący. Został zbudowany w oparciu o trzy główne abstrakcje:

  • bufory (buffers),
  • kanały (channels),
  • selektory (selectors).

Bufor

W kontekście niskopoziomowych mechanizmów

W kontekście niskopoziomowych mechanizmów bufor to blok pamięci, w którym tymczasowo umieszcza się dane odbierane lub wysyłane do zewnętrznego urządzenia. Jest swego rodzaju pośrednikiem, który umożliwia komunikację urządzeń przetwarzających informacje z różnymi prędkościami.

Bardzo dobrą analogią, dzięki której można wyjaśnić znaczenie buforów, jest analogia cieknącego wiadra (leaky bucket analogy).

Wyobraźmy sobie, że mamy wiaderko z dziurą na dnie. Możemy do niego wlewać wodę z dowolną prędkością, lecz prędkość jej uciekania zdeterminowana jest przez rozmiar dziury. W analogi tej wiaderko to bufor, prędkość wlewania wody do niego to prędkość, z jaką proces chce wysyłać dane, a rozmiar dziury to ograniczenia interfejsu sieciowego.

W kontekście Javy

W kontekście Javy bufor to nic innego jak klasa, która opakowuje swój niskopoziomowy odpowiednik w stan i metody służące do jego łatwiejszej manipulacji. Prócz zawartości, Java śledzi również takie własności bufora jak jego:

  • pozycję (position) – indeks następnego elementu, który ma zostać odczytany/zapisany,
  • limit (limit) – indeks pierwszego elementu, który ma nie być odczytany/zapisany,
  • ładowność (capacity) – rozmiar bufora.

Aby lepiej zrozumieć te charakterystyki, a także zobaczyć w akcji działanie pewnych metod służących do manipulacji buforem, spójrzmy na przykład.

Kanał

Kanał reprezentuje połączenie z obiektem zdolnym do przeprowadzania operacji wejścia/wyjścia. Korzysta z buforów, z których czyta dane przeznaczone do wysłania, i do których zapisuje otrzymane informacje. Tak jak Bufor jest opakowaniem natywnego bufora, tak kanał jest warstwą abstrakcji, bezpośrednio pod którą kryje się deskryptor pliku.

Selektor

Niektóre kanały (m.in. te związane z komunikacją sieciową) rozszerzają klasę abstrakcyjną  SelectableChannel.  Kanały reprezentowane przez instancje tej klasy mają taką własność, że można je ustawić w nieblokujący tryb pracy i multipleksować. Narzędziem, które służy do multipleksacji, jest selektor.

Zasada działania selektora jest bardzo prosta. Po utworzeniu rejestrujemy w nim wszystkie kanały, które chcemy nasłuchiwać. W wyniku tej operacji każdemu kanałowi przypisywany jest selectionKey – obiekt jednoznacznie identyfikujący go i zawierający informacje o jego stanie (np. gotowość do przyjęcia danych). Następnie cyklicznie odpytujemy selektor o listę kluczy, których kanały są gotowe do przeprowadzenia operacji wejścia/wyjścia.

Przykład

W ramach podsumowania złożymy w całość wszystkie opisane powyżej elementy i stworzymy prosty, nieblokujący, jednowątkowy serwer. Serwer ten przetransformuje każdy otrzymany tekst na wersję UPPERCASE i odeśle go z powrotem do klienta.

2 Comments

  1. dobry temat 🙂

    następny blog-post już o implementacji takiego server w czystym C w oparciu o epoll ?

  2. Zamiast o epoll to może libevent lub gio ?

Dodaj komentarz

Your email address will not be published.

*

© 2019 Kodujże!

Theme by Anders NorenUp ↑