Java IO versus NIO

1. Overzicht

Het afhandelen van invoer en uitvoer zijn veelvoorkomende taken voor Java-programmeurs. In deze tutorial kijken we naar de origineel java.io (IO) bibliotheken en de nieuwere java.nio (NIO) bibliotheken en hoe ze verschillen wanneer ze via een netwerk communiceren.

2. Belangrijkste kenmerken

Laten we beginnen met het bekijken van de belangrijkste kenmerken van beide pakketten.

2.1. IO - java.io

De java.io pakket werd geïntroduceerd in Java 1.0, met Lezer geïntroduceerd in Java 1.1. Het zorgt voor:

  • InputStream en OutputStream - die gegevens byte per keer leveren
  • Lezer en auteur - gemakswrappers voor de streams
  • blokkeermodus - om te wachten op een volledig bericht

2.2. NIO - java.nio

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

  • Bufferom stukjes gegevens tegelijk te lezen
  • CharsetDecoder - voor het toewijzen van onbewerkte bytes aan / van leesbare tekens
  • Kanaal - voor communicatie met de buitenwereld
  • Selector - om multiplexen op een Selecteerbaar kanaal en geef toegang tot alle Kanaals die klaar zijn voor I / O
  • niet-blokkerende modus - om te lezen wat klaar is

Laten we nu eens kijken hoe we elk van deze pakketten gebruiken wanneer we gegevens naar een server sturen of de reactie ervan lezen.

3. Configureer onze testserver

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

We zullen het configureren om naar onze verzoeken te luisteren en ons antwoorden te sturen, net zoals een echte webserver dat zou doen. We zullen ook een dynamische poort gebruiken, zodat we geen conflicten veroorzaken met services op onze lokale computer.

Laten we de Maven-afhankelijkheid voor WireMock toevoegen met test reikwijdte:

 com.github.tomakehurst wiremock-jre8 2.26.3 test 

Laten we in een testklasse een JUnit definiëren @Regel om WireMock op een vrije poort te starten. We zullen het dan configureren om ons een HTTP 200-antwoord te retourneren wanneer we om een ​​vooraf gedefinieerde bron vragen, met de berichttekst als tekst in JSON-indeling:

@Rule publiek WireMockRule wireMockRule = nieuwe WireMockRule (wireMockConfig (). DynamicPort ()); private String REQUESTED_RESOURCE = "/test.json"; @Before public void setup () {stubFor (get (urlEqualTo (REQUESTED_RESOURCE)) .willReturn (aResponse () .withStatus (200) .withBody ("{\" response \ ": \" Het werkte! \ "}")) ); }

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

4. IO blokkeren - java.io

Laten we eens kijken hoe het originele blokkerende IO-model werkt door enkele gegevens 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 Maak een Stopcontact om toegang te krijgen tot de haven waar onze WireMock-server naar luistert:

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

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

Laten we nu open een OutputStream op het stopcontact, verpakt in een OutputStreamWriter en geef het door aan een PrintWriter om ons bericht te schrijven. En laten we ervoor zorgen dat we de buffer leegmaken zodat ons verzoek wordt verzonden:

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

4.2. Wacht op de reactie

Laten we open een InputStreamop het stopcontact om toegang te krijgen tot het antwoord, leest u de stream met een BufferedReader, en bewaar het in een StringBuilder:

InputStream serverInput = socket.getInputStream (); BufferedReader-lezer = nieuwe BufferedReader (nieuwe InputStreamReader (serverInput)); StringBuilder ourStore = nieuwe StringBuilder ();

Laten we gebruiken reader.readLine () te blokkeren, wachtend op een volledige regel, voeg dan de regel toe aan onze winkel. We blijven lezen totdat we een nul, wat het einde van de stream aangeeft:

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

5. Niet-blokkerende IO - java.nio

Laten we nu eens kijken hoe de nio pakket niet-blokkerende IO-model werkt met hetzelfde voorbeeld.

Deze keer zullen we Maak een java.nio.channel.SocketChannel om toegang te krijgen tot de haven op onze server in plaats van een java.net.Socket, en geef het een InetSocketAddress.

5.1. Stuur een verzoek

Laten we eerst onze openen SocketChannel:

InetSocketAddress adres = nieuw InetSocketAddress ("localhost", wireMockRule.port ()); SocketChannel socketChannel = SocketChannel.open (adres);

En laten we nu een standaard UTF-8 nemen Tekenset om ons bericht te coderen en te 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 onbewerkte buffers.

Omdat we tekst gaan verwerken, hebben we een ByteBuffer voor de onbewerkte 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 heeft ruimte over als de gegevens worden verzonden in een multi-byte tekenset.

Merk op dat als we bijzonder snelle prestaties nodig hebben, we een MappedByteBuffer in native geheugen met ByteBuffer.allocateDirect (). In ons geval gebruiken we echter toewijzen () van de standaardhoop is snel genoeg.

Als we met buffers te maken hebben, moeten we weten hoe groot de buffer is (de capaciteit), waar we zijn in de buffer (de huidige positie), en hoe ver we kunnen gaan (de grens).

Dus laten we voorlezen uit onze SocketChannel, het doorgeven van onze ByteBuffer om onze gegevens op te slaan. Onze lezen van de SocketChannel zal eindigen met onze ByteBuffer‘S huidige positie ingesteld op de volgende byte om naar te schrijven (net na de laatste geschreven byte), maar met zijn limiet ongewijzigd:

socketChannel.read (byteBuffer)

Onze SocketChannel.read () geeft het aantal gelezen bytes terug die in onze buffer kunnen worden geschreven. Dit wordt -1 als het stopcontact is losgekoppeld.

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

Om ervoor te zorgen dat we beginnen met lezen vanaf de juiste plaats in de buffer, gebruiken we Buffer.flip() om onze ByteBuffer‘Huidige positie op nul en de limiet tot de laatste byte die is geschreven door de SocketChannel. We zullen dan de bufferinhoud opslaan met behulp van onze storeBufferContents methode, die we later zullen bekijken. Ten slotte gebruiken we buffer.compact () om de buffer te comprimeren en de huidige positie klaar te maken voor onze volgende lezing van de SocketChannel.

Omdat onze gegevens in delen kunnen aankomen, laten we onze buffer-leescode in een lus met beëindigingsvoorwaarden wikkelen om te controleren of onze socket nog steeds is aangesloten of dat we zijn losgekoppeld maar nog steeds gegevens 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 dichtbij() onze socket (tenzij we het hebben geopend in een try-with-resources-blok):

socketChannel.close ();

5.3. Gegevens uit onze buffer opslaan

Het antwoord van de server bevat headers, waardoor de hoeveelheid gegevens de grootte van onze buffer kan overschrijden. Dus we gebruiken een StringBuilder om onze complete boodschap op te bouwen zodra deze aankomt.

Om ons bericht op te slaan, gaan we eerst decodeer de onbewerkte bytes in tekens in onze CharBuffer. Vervolgens draaien we de aanwijzers om zodat we onze karaktergegevens kunnen lezen en deze aan ons uitbreidingsbestand toevoegen StringBuilder. Ten slotte wissen we het CharBuffer klaar voor de volgende schrijf- / leescyclus.

Dus laten we nu onze complete storeBufferContents () methode passeren in onze buffers, CharsetDecoder, en StringBuilder:

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

6. Conclusie

In dit artikel hebben we gezien hoe de origineel java.io modelblokken, wacht op een verzoek en gebruikt Strooms om de ontvangen gegevens te manipuleren.

In tegenstelling tot, de java.nio bibliotheken zorgen voor niet-blokkerende communicatie gebruik makend van Buffers en Kanaals en kan directe geheugentoegang bieden voor snellere prestaties. Met deze snelheid komt echter de extra complexiteit van het afhandelen van buffers.

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