Overzicht

Handelen met invoer en uitvoer zijn veel voorkomende taken voor Java programmeurs. In deze tutorial bekijken we de originele java.io (IO) bibliotheken en de nieuwere java.nio (NIO) bibliotheken en hoe ze verschillen bij het communiceren over een netwerk.

Key Features

Laten we beginnen met te kijken naar de belangrijkste kenmerken van beide pakketten.

2.1. IO – java.io

Het java.io-pakket werd geïntroduceerd in Java 1.0, met Reader geïntroduceerd in Java 1.1. Het biedt:

  • InputStream en OutputStream – die gegevens een byte per keer
  • Reader en Writer – gemak wrappers voor de streams
  • blocking mode – om te wachten op een volledig bericht

2.2. NIO – java.nio

Het java.nio pakket werd geïntroduceerd in Java 1.4 en bijgewerkt in Java 1.7 (NIO.2) met verbeterde bestandsoperaties en een ASynchronousSocketChannel. Het biedt:

  • Buffer – om brokken data tegelijk te lezen
  • CharsetDecoder – voor het mappen van ruwe bytes naar/van leesbare karakters
  • Channel – voor communicatie met de buitenwereld
  • Selector – om multiplexing op een SelectableChannel mogelijk te maken en toegang te bieden tot alle Channels die gereed zijn voor I/O
  • non-blocking mode – om te lezen wat klaar is

Nu gaan we eens kijken hoe we elk van deze pakketten gebruiken als we data naar een server sturen of zijn antwoord lezen.

Configureer onze testserver

Hier gebruiken we WireMock om een andere server te simuleren, zodat we onze tests onafhankelijk kunnen uitvoeren.

We configureren hem om te luisteren naar onze verzoeken en om ons antwoorden te sturen, net zoals een echte webserver dat zou doen. We gebruiken ook een dynamische poort, zodat we niet in conflict komen met services op onze lokale machine.

Laten we de Maven dependency voor WireMock toevoegen met test scope:

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

In een test class, laten we een JUnit @Rule definiëren om WireMock op te starten op een vrije poort. We zullen het dan configureren om ons een HTTP 200 antwoord terug te sturen wanneer we om een voorgedefinieerde bron vragen, met het bericht als een tekst in JSON formaat:

@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!\" }")));}

Nu dat we onze mock server opgezet hebben, zijn we klaar om enkele tests uit te voeren.

Blocking IO – java.io

Laten we eens kijken hoe het originele blocking IO model werkt door wat data van een website te lezen. We gebruiken een java.net.Socket om toegang te krijgen tot een van de poorten van het besturingssysteem.

4.1. Stuur een verzoek

In dit voorbeeld, zullen we een GET verzoek maken om onze bronnen op te halen. Laten we eerst een Socket aanmaken om toegang te krijgen tot de poort waar onze WireMock server op luistert:

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

Voor normale HTTP of HTTPS communicatie zou de poort 80 of 443 zijn. In dit geval gebruiken we echter wireMockRule.port() om toegang te krijgen tot de dynamische poort die we eerder hebben ingesteld.

Nu openen we een OutputStream op de socket, gewikkeld in een OutputStreamWriter en geven die door aan een PrintWriter om ons bericht te schrijven. En laten we ervoor zorgen dat we de buffer spoelen, zodat ons verzoek wordt verzonden:

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. Wacht op het antwoord

Open een InputStream op de socket om toegang te krijgen tot het antwoord, lees de stream met een BufferedReader, en sla het op in een StringBuilder:

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

Gebruik reader.readLine() om te blokkeren, wachtend op een complete regel, en voeg dan de regel toe aan onze opslag. We blijven lezen totdat we een null krijgen, wat het einde van de stream aangeeft:

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

Non-Blocking IO – java.nio

Nu, laten we eens kijken hoe het nio pakket’s non-blocking IO model werkt met het zelfde voorbeeld.

Deze keer maken we een java.nio.channel.SocketChannel om toegang te krijgen tot de poort op onze server in plaats van een java.net.Socket, en geven het een InetSocketAddress.

5.1. Verstuur een Verzoek

Laten we eerst onze SocketChannel openen:

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

En nu, laten we een standaard UTF-8 Charset coderen en ons bericht schrijven:

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

5.2. Lees het antwoord

Nadat we het verzoek hebben verzonden, kunnen we het antwoord lezen in niet-blokkerende modus, met behulp van ruwe buffers.

Omdat we tekst zullen verwerken, hebben we een ByteBuffer nodig voor de ruwe bytes en een CharBuffer voor de geconverteerde karakters (geholpen door een CharsetDecoder):

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

Onze CharBuffer zal ruimte over hebben als de gegevens worden verzonden in een multi-byte karakterset.

Merk op dat als we bijzonder snelle prestaties nodig hebben, we een MappedByteBuffer in native geheugen kunnen maken met ByteBuffer.allocateDirect(). In ons geval is het echter snel genoeg om allocate() uit de standaard heap te gebruiken.

Wanneer we met buffers te maken hebben, moeten we weten hoe groot de buffer is (de capaciteit), waar we ons in de buffer bevinden (de huidige positie), en hoe ver we kunnen gaan (de limiet).

Dus, laten we lezen van onze SocketChannel, waarbij we onze ByteBuffer doorgeven om onze gegevens op te slaan. Ons lezen van de SocketChannel zal eindigen met de huidige positie van onze ByteBuffer ingesteld op de volgende byte om naar te schrijven (net na de laatst geschreven byte), maar met zijn limiet ongewijzigd:

socketChannel.read(byteBuffer)

Onze SocketChannel.read() retourneert het aantal gelezen bytes dat in onze buffer kon worden geschreven. Dit zal -1 zijn als de socket was losgekoppeld.

Wanneer onze buffer geen ruimte meer heeft omdat we nog niet alle data hebben verwerkt, dan zal SocketChannel.read() nul bytes gelezen teruggeven, maar onze buffer.position() zal nog steeds groter zijn dan nul.

Om er zeker van te zijn dat we vanaf de juiste plaats in de buffer beginnen te lezen, gebruiken we Buffer.flip() om de huidige positie van onze ByteBuffer op nul te zetten en de limiet op de laatste byte die door de SocketChannel werd geschreven. Daarna slaan we de inhoud van de buffer op met onze storeBufferContents methode, die we later zullen bekijken. Tenslotte gebruiken we buffer.compact() om de buffer te verkleinen en de huidige positie klaar te zetten voor onze volgende leesopdracht van de SocketChannel.

Omdat onze data in delen kan aankomen, wikkelen we onze buffer-lees code in een lus met terminatie voorwaarden om te controleren of onze socket nog steeds verbonden is of dat we zijn losgekoppeld maar nog steeds data in onze buffer hebben:

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

En laten we niet vergeten om onze socket te sluiten() (tenzij we hem hebben geopend in een try-met-resources blok):

socketChannel.close();

5.3. Het opslaan van gegevens uit onze buffer

Het antwoord van de server zal headers bevatten, waardoor de hoeveelheid gegevens de grootte van onze buffer kan overschrijden. Dus gebruiken we een StringBuilder om ons complete bericht op te bouwen als het binnenkomt.

Om ons bericht op te slaan, decoderen we eerst de ruwe bytes in karakters in onze CharBuffer. Dan draaien we de pointers om, zodat we onze karaktergegevens kunnen lezen, en voegen die toe aan onze uitbreidbare StringBuilder. Tenslotte wissen we de CharBuffer, klaar voor de volgende schrijf/lees cyclus.

Dus nu, laten we onze complete storeBufferContents() methode implementeren door onze buffers, CharsetDecoder, en StringBuilder in te voeren:

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

Conclusie

In dit artikel hebben we gezien hoe het originele java.io model blokkeert, op een verzoek wacht en Streams gebruikt om de ontvangen gegevens te manipuleren.

De java.nio bibliotheken daarentegen staan niet-blokkerende communicatie toe met Buffers en Channels en kunnen directe geheugentoegang bieden voor snellere prestaties. Met deze snelheid komt echter de extra complexiteit van het omgaan met buffers.

Zoals gebruikelijk is de code voor dit artikel beschikbaar op GitHub.

Ga aan de slag met Spring 5 en Spring Boot 2, via de Leer Spring cursus:

>> CHECK OUT THE COURSE

Geef een antwoord

Het e-mailadres wordt niet gepubliceerd.