Przegląd

Obsługa danych wejściowych i wyjściowych jest powszechnym zadaniem dla programistów Javy. W tym poradniku przyjrzymy się oryginalnym bibliotekom java.io (IO) i nowszym java.nio (NIO) oraz temu, czym różnią się one w przypadku komunikacji przez sieć.

Kluczowe cechy

Zacznijmy od przyjrzenia się kluczowym cechom obu pakietów.

2.1. IO – java.io

Pakiet java.io został wprowadzony w Javie 1.0, a Reader w Javie 1.1. Dostarcza on:

  • InputStream i OutputStream – które dostarczają dane po jednym bajcie na raz
  • Reader i Writer – wygodne wrappery dla strumieni
  • blocking mode – do oczekiwania na kompletny komunikat

2.2. NIO – java.nio

Pakiet java.nio został wprowadzony w Javie 1.4 i zaktualizowany w Javie 1.7 (NIO.2) o ulepszone operacje na plikach i ASynchronousSocketChannel. Zapewnia on:

  • Buffer – do odczytu kawałków danych na raz
  • CharsetDecoder – do mapowania surowych bajtów na/z czytelnych znaków
  • Channel – do komunikacji ze światem zewnętrznym
  • Selector – do włączania multipleksowania na SelectableChannel i zapewnienia dostępu do wszelkich Channels, które są gotowe do I/O
  • non-.tryb blokujący – do odczytu tego, co jest gotowe

Przyjrzyjrzyjmy się teraz, jak używamy każdego z tych pakietów, gdy wysyłamy dane do serwera lub odczytujemy jego odpowiedź.

Konfiguracja naszego serwera testowego

Tutaj użyjemy WireMock do symulacji innego serwera, abyśmy mogli przeprowadzić nasze testy niezależnie.

Skonfigurujemy go tak, aby nasłuchiwał naszych żądań i wysyłał nam odpowiedzi, tak jak prawdziwy serwer WWW. Użyjemy także dynamicznego portu, aby nie wchodzić w konflikt z żadnymi usługami na naszej lokalnej maszynie.

Dodajmy zależność Maven dla WireMock z zakresem testowym:

<dependency> <groupId>com.github.tomakehurst</groupId> <artifactId>wiremock-jre8</artifactId> <version>2.26.3</version> <scope>test</scope></dependency>

W klasie testowej zdefiniujmy JUnit @Rule, aby uruchomić WireMock na wolnym porcie. Następnie skonfigurujemy go tak, aby zwracał nam odpowiedź HTTP 200, gdy poprosimy o zdefiniowany wcześniej zasób, z ciałem wiadomości w postaci tekstu w formacie JSON:

@Rule public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort());private String REQUESTED_RESOURCE = "/test.json";@Beforepublic void setup() { stubFor(get(urlEqualTo(REQUESTED_RESOURCE)) .willReturn(aResponse() .withStatus(200) .withBody("{ \"response\" : \"It worked!\" }")));}

Teraz, gdy mamy już skonfigurowany nasz serwer, jesteśmy gotowi do przeprowadzenia kilku testów.

Blocking IO – java.io

Przyjrzyjrzyjmy się, jak działa oryginalny model blokowania IO, odczytując niektóre dane ze strony internetowej. Użyjemy java.net.Socket, aby uzyskać dostęp do jednego z portów systemu operacyjnego.

4.1. Wysyłanie żądania

W tym przykładzie utworzymy żądanie GET, aby pobrać nasze zasoby. Po pierwsze, utwórzmy Socket, aby uzyskać dostęp do portu, na którym nasłuchuje nasz serwer WireMock:

Socket socket = new Socket("localhost", wireMockRule.port())

W przypadku normalnej komunikacji HTTP lub HTTPS, portem tym będzie 80 lub 443. Jednak w tym przypadku używamy wireMockRule.port(), aby uzyskać dostęp do dynamicznego portu, który ustawiliśmy wcześniej.

Teraz otwórzmy OutputStream na gnieździe, zawińmy w OutputStreamWriter i przekażmy go do PrintWriter, aby zapisać naszą wiadomość. I upewnijmy się, że przepłuczemy bufor, aby nasze żądanie zostało wysłane:

OutputStream clientOutput = socket.getOutputStream();PrintWriter writer = new PrintWriter(new OutputStreamWriter(clientOutput));writer.print("GET " + TEST_JSON + " HTTP/1.0\r\n\r\n");writer.flush();

4.2. Wait for the Response

Otwórzmy InputStream na gnieździe, aby uzyskać dostęp do odpowiedzi, odczytajmy strumień za pomocą BufferedReader i zapiszmy go w StringBuilder:

InputStream serverInput = socket.getInputStream();BufferedReader reader = new BufferedReader(new InputStreamReader(serverInput));StringBuilder ourStore = new StringBuilder();

Użyjmy reader.readLine(), aby zablokować, czekając na pełną linię, a następnie dołączmy linię do naszego magazynu. Będziemy czytać tak długo, aż otrzymamy null, który oznacza koniec strumienia:

for (String line; (line = reader.readLine()) != null;) { ourStore.append(line); ourStore.append(System.lineSeparator());}

Non-Blocking IO – java.nio

Przyjrzyjrzyjmy się teraz, jak działa nieblokujący model IO pakietu nio na tym samym przykładzie.

Tym razem stworzymy java.nio.channel.SocketChannel, aby uzyskać dostęp do portu na naszym serwerze zamiast java.net.Socket, i przekażemy mu InetSocketAddress.

5.1. Wyślij żądanie

Po pierwsze, otwórzmy nasz SocketChannel:

InetSocketAddress address = new InetSocketAddress("localhost", wireMockRule.port());SocketChannel socketChannel = SocketChannel.open(address);

A teraz, weźmy standardowy UTF-8 Charset, aby zakodować i zapisać naszą wiadomość:

Charset charset = StandardCharsets.UTF_8;socket.write(charset.encode(CharBuffer.wrap("GET " + REQUESTED_RESOURCE + " HTTP/1.0\r\n\r\n")));

5.2. Odczytaj odpowiedź

Po wysłaniu żądania, możemy odczytać odpowiedź w trybie nieblokującym, używając surowych buforów.

Ponieważ będziemy przetwarzać tekst, będziemy potrzebować ByteBuffer dla surowych bajtów i CharBuffer dla przekonwertowanych znaków (wspomaganych przez CharsetDecoder):

ByteBuffer byteBuffer = ByteBuffer.allocate(8192);CharsetDecoder charsetDecoder = charset.newDecoder();CharBuffer charBuffer = CharBuffer.allocate(8192);

Nasz CharBuffer będzie miał wolne miejsce, jeśli dane są wysyłane w wielobajtowym zestawie znaków.

Zauważ, że jeśli potrzebujemy szczególnie szybkiej wydajności, możemy utworzyć MappedByteBuffer w pamięci natywnej używając ByteBuffer.allocateDirect(). Jednakże, w naszym przypadku, użycie allocate() ze standardowej sterty jest wystarczająco szybkie.

Gdy mamy do czynienia z buforami, musimy wiedzieć jak duży jest bufor (pojemność), gdzie jesteśmy w buforze (bieżąca pozycja), i jak daleko możemy się posunąć (limit).

Więc, odczytajmy z naszego SocketChannel, przekazując mu nasz ByteBuffer do przechowywania naszych danych. Nasz odczyt z SocketChannel zakończy się z aktualną pozycją naszego ByteBuffera ustawioną na następny bajt do zapisu (zaraz po ostatnim zapisanym bajcie), ale z jego limitem niezmienionym:

socketChannel.read(byteBuffer)

Nasz SocketChannel.read() zwraca liczbę odczytanych bajtów, które mogły być zapisane do naszego bufora. Będzie to -1, jeśli gniazdo zostało rozłączone.

Gdy nasz bufor nie ma już miejsca, ponieważ nie przetworzyliśmy jeszcze wszystkich jego danych, wtedy SocketChannel.read() zwróci zero przeczytanych bajtów, ale nasz bufor.position() nadal będzie większy od zera.

Aby upewnić się, że zaczniemy czytać z właściwego miejsca w buforze, użyjemy Buffer.flip() aby ustawić aktualną pozycję naszego ByteBuffera na zero, a jego limit na ostatni bajt, który został zapisany przez SocketChannel. Następnie zapiszemy zawartość bufora używając naszej metody storeBufferContents, której przyjrzymy się później. Na koniec, użyjemy buffer.compact() aby skompaktować bufor i ustawić aktualną pozycję gotową na nasz następny odczyt z SocketChannel.

Ponieważ nasze dane mogą przychodzić w częściach, zawińmy nasz kod odczytu bufora w pętlę z warunkami zakończenia, aby sprawdzić, czy nasze gniazdo jest wciąż połączone lub czy zostaliśmy rozłączeni, ale wciąż mamy dane w naszym buforze:

while (socketChannel.read(byteBuffer) != -1 || byteBuffer.position() > 0) { byteBuffer.flip(); storeBufferContents(byteBuffer, charBuffer, charsetDecoder, ourStore); byteBuffer.compact();}

I nie zapomnijmy zamknąć() naszego gniazda (chyba że otworzyliśmy je w bloku try-with-resources):

socketChannel.close();

5.3. Przechowywanie danych z naszego bufora

Odpowiedź z serwera będzie zawierała nagłówki, co może spowodować, że ilość danych przekroczy rozmiar naszego bufora. Tak więc, użyjemy StringBuilder do zbudowania naszej kompletnej wiadomości w momencie jej nadejścia.

Aby przechować naszą wiadomość, najpierw zdekodujemy surowe bajty na znaki w naszym CharBuffer. Następnie odwrócimy wskaźniki tak, abyśmy mogli odczytać nasze dane znaków i dołączymy je do naszego rozszerzalnego StringBuilder. Na koniec, wyczyścimy CharBuffer gotowy do następnego cyklu zapisu/odczytu.

Więc teraz, zaimplementujmy naszą kompletną metodę storeBufferContents() przekazując nasze bufory, CharsetDecoder i StringBuilder:

void storeBufferContents(ByteBuffer byteBuffer, CharBuffer charBuffer, CharsetDecoder charsetDecoder, StringBuilder ourStore) { charsetDecoder.decode(byteBuffer, charBuffer, true); charBuffer.flip(); ourStore.append(charBuffer); charBuffer.clear();}

Podsumowanie

W tym artykule, zobaczyliśmy jak oryginalny model java.io blokuje się, czeka na żądanie i używa Streamów do manipulowania danymi, które otrzymuje.

W przeciwieństwie do tego, biblioteki java.nio pozwalają na nieblokującą komunikację przy użyciu Buforów i Kanałów i mogą zapewnić bezpośredni dostęp do pamięci dla szybszej wydajności. Jednak wraz z tą szybkością przychodzi dodatkowa złożoność obsługi buforów.

Jak zwykle, kod do tego artykułu jest dostępny na GitHub.

Zacznij od Spring 5 i Spring Boot 2, dzięki kursowi Learn Spring:

>> CHECK OUT THE COURSE

.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.