Java IO vs NIO

1. Pregled

Rukovanje ulazom i izlazom uobičajeni su zadaci Java programera. U ovom uputstvu pogledat ćemo izvornik java.io (IO) knjižnice i novije java.nio (NIO) knjižnice i kako se razlikuju u komunikaciji putem mreže.

2. Ključne značajke

Počnimo s razgledanjem ključnih značajki oba paketa.

2.1. IO - java.io

The java.io paket uveden je u Javi 1.0, sa Čitač predstavljen u Javi 1.1. Sadrži:

  • InputStream i Izlazni tok - koji pružaju podatke po jedan bajt
  • Čitač i Pisac - praktični omoti za potoke
  • način blokiranja - za čekanje potpune poruke

2.2. NIO - java.nio

The java.nio paket predstavljen je u Javi 1.4 i ažuriran u Javi 1.7 (NIO.2) s poboljšanim operacijama datoteka i ASynchronousSocketChannel. Sadrži:

  • Puferza čitanje dijelova podataka odjednom
  • CharsetDecoder - za mapiranje sirovih bajtova u / iz čitljivih znakova
  • Kanal - za komunikaciju s vanjskim svijetom
  • Selektor - omogućiti multipleksiranje na a Kanal koji se može odabrati i omogućiti pristup bilo kojem Kanalkoji su spremni za I / O
  • način neblokiranja - za čitanje onoga što je spremno

Pogledajmo sada kako koristimo svaki od ovih paketa kada podatke šaljemo poslužitelju ili čitamo njegov odgovor.

3. Konfigurirajte naš testni poslužitelj

Ovdje ćemo koristiti WireMock za simulaciju drugog poslužitelja kako bismo mogli samostalno pokretati naše testove.

Konfigurirat ćemo ga da sluša naše zahtjeve i da nam šalje odgovore baš kao što bi to činio pravi web poslužitelj. Također ćemo upotrijebiti dinamički priključak kako se ne bismo sukobili s bilo kojim uslugama na našem lokalnom računalu.

Dodajmo ovisnost Mavena za WireMock s test opseg:

 com.github.tomakehurst wiremock-jre8 2.26.3 test 

U testnoj klasi definirajmo JUnit @Pravilo za pokretanje WireMocka na besplatnoj luci. Zatim ćemo ga konfigurirati da nam vrati HTTP 200 odgovor kada zatražimo unaprijed definirani resurs, s tijelom poruke kao nekim tekstom u JSON formatu:

@Rule public WireMockRule wireMockRule = new WireMockRule (wireMockConfig (). DynamicPort ()); privatni niz REQUESTED_RESOURCE = "/test.json"; @Prije javne void setup () {stubFor (get (urlEqualTo (REQUESTED_RESOURCE)) .willReturn (aResponse () .withStatus (200) .withBody ("{\" response \ ": \" Uspjelo je! \ "}")) ); }

Sad kad smo postavili naš lažni poslužitelj, spremni smo za pokretanje nekih testova.

4. Blokiranje IO - java.io

Pogledajmo kako funkcionira izvorni model blokiranja IO čitanjem nekih podataka s web mjesta. Koristit ćemo a java.net.Utičnica za pristup jednom od porta operativnog sustava.

4.1. Pošaljite zahtjev

U ovom ćemo primjeru stvoriti GET zahtjev za preuzimanje naših resursa. Prvo, krenimo stvoriti Utičnica za pristup luci koje naš WireMock poslužitelj sluša:

Socket socket = nova utičnica ("localhost", wireMockRule.port ())

Za normalnu HTTP ili HTTPS komunikaciju port bi bio 80 ili 443. Međutim, u ovom slučaju koristimo wireMockRule.port () za pristup dinamičkom priključku koji smo ranije postavili.

Ajmo sad otvori an Izlazni tok na utičnici, zamotan u OutputStreamWriter i proslijedite ga a PrintWriter da napišu našu poruku. I pobrinimo se da ispraznemo međuspremnik tako da se pošalje naš zahtjev:

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

4.2. Pričekajte odgovor

Idemo otvori an InputStreamna utičnici da biste pristupili odgovoru, pročitajte stream s BufferedReaderi spremite ga u StringBuilder:

InputStream serverInput = socket.getInputStream (); Čitač BufferedReader = novi BufferedReader (novi InputStreamReader (serverInput)); StringBuilder ourStore = novi StringBuilder ();

Iskoristimo reader.readLine () za blokiranje, čekajući kompletan red, a zatim dodajte red našoj trgovini. Nastavit ćemo čitati dok ne dobijemo null, što označava kraj toka:

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

5. IO bez blokiranja - java.nio

Pogledajmo sada kako nio paket koji ne blokira IO model radi s istim primjerom.

Ovaj put ćemo stvoriti java.nio.kanal.SocketChannel za pristup luci na našem poslužitelju umjesto java.net.Utičnicai položite ga InetSocketAddress.

5.1. Pošaljite zahtjev

Prvo, otvorimo našu SocketChannel:

Adresa InetSocketAddress = nova InetSocketAddress ("localhost", wireMockRule.port ()); SocketChannel socketChannel = SocketChannel.open (adresa);

A sada, uzmimo standardni UTF-8 Charset za kodiranje i pisanje naše poruke:

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

5.2. Pročitajte odgovor

Nakon što pošaljemo zahtjev, možemo pročitati odgovor u neblokirajućem načinu, koristeći sirove međuspremnike.

Budući da ćemo obrađivati ​​tekst, trebat će nam ByteBuffer za sirove bajtove i a CharBuffer za pretvorene znakove (uz pomoć a CharsetDecoder):

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

Naše CharBuffer ostat će prostora ako se podaci šalju u višebajtnom skupu znakova.

Imajte na umu da ako trebamo posebno brze performanse, možemo stvoriti MappedByteBuffer u zavičajnoj memoriji koristeći ByteBuffer.allocateDirect (). Međutim, u našem slučaju, pomoću dodijeliti () iz standardne hrpe je dovoljno brz.

Kad se bavimo međuspremnicima, moramo znati kolika je veličina međuspremnika (kapacitet), gdje smo u tamferu (trenutna pozicija), i dokle možemo stići (ograničenje).

Pa, krenimo čitati iz našeg SocketChannel, prosljeđujući ga našem ByteBuffer za pohranu naših podataka. Naše čitati od SocketChannel završit će s našim ByteBuffer‘S trenutni položaj postavljen na sljedeći bajt u koji želite upisati (odmah nakon zadnjeg napisanog bajta), ali s nepromijenjenom granicom:

socketChannel.read (byteBuffer)

Naše SocketChannel.read () vraća broj pročitanih bajtova to bi se moglo zapisati u naš međuspremnik. To će biti -1 ako je utičnica bila isključena.

Tada u našem međuspremniku nema prostora jer još nismo obradili sve njegove podatke SocketChannel.read () vratit će nula bajtova pročitanih, ali naš buffer.position () i dalje će biti veća od nule.

Da bismo bili sigurni da čitanje započinjemo s pravog mjesta u međuspremniku, upotrijebit ćemo Odbojnik.flip() za postavljanje našeg ByteBufferTrenutni položaj na nulu i ograničenje na zadnji bajt koji je napisao SocketChannel. Zatim ćemo spremiti sadržaj međuspremnika pomoću našeg storeBufferContents metodu, koju ćemo pogledati kasnije. Na kraju ćemo upotrijebiti buffer.compact () da kompaktiramo međuspremnik i postavimo trenutni položaj spreman za sljedeće čitanje iz SocketChannel.

Budući da naši podaci mogu stizati u dijelovima, umotajmo kod za čitanje međuspremnika u petlju s uvjetima prekida kako bismo provjerili je li naša utičnica još uvijek povezana ili smo prekinuti vezu, ali još uvijek imamo podataka u međuspremniku:

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

I ne zaboravimo Zatvoriti() naša utičnica (osim ako je nismo otvorili u bloku try-with-resources):

socketChannel.close ();

5.3. Pohranjivanje podataka iz našeg međuspremnika

Odgovor poslužitelja sadržavat će zaglavlja zbog kojih količina podataka može premašiti veličinu našeg međuspremnika. Dakle, koristit ćemo a StringBuilder kako bismo izgradili našu cjelovitu poruku čim stigne.

Da pohranimo svoju poruku, mi prvo dekodirati sirove bajtove u znakove u našem CharBuffer. Zatim ćemo okrenuti pokazivače kako bismo mogli čitati podatke o znakovima i dodati ih proširivom StringBuilder. Na kraju ćemo očistiti CharBuffer spremno za sljedeći ciklus pisanja / čitanja.

Dakle, hajde da implementiramo naše kompletno storeBufferContents () metoda koja prolazi u naše međuspremnike, 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 (); }

6. Zaključak

U ovom smo članku vidjeli kako izvornik java.io blokovi modela, čeka zahtjev i koristi Streams za manipulaciju podacima koje prima.

U kontrastu, the java.nio knjižnice omogućuju neblokirajuću komunikaciju koristeći Pufers i Kanals i mogu pružiti izravan pristup memoriji za brže performanse. Međutim, s ovom brzinom dolazi i dodatna složenost rukovanja odbojnicima.

Kao i obično, kôd za ovaj članak dostupan je na GitHubu.