Overview

Manipularea intrărilor și ieșirilor sunt sarcini comune pentru programatorii Java. În acest tutorial, vom analiza bibliotecile originale java.io (IO) și bibliotecile mai noi java.nio (NIO) și modul în care acestea diferă atunci când comunică printr-o rețea.

Caracteristici cheie

Să începem prin a analiza caracteristicile cheie ale ambelor pachete.

2.1. IO – java.io

Pachetul java.io a fost introdus în Java 1.0, iar Reader a fost introdus în Java 1.1. Acesta oferă:

  • InputStream și OutputStream – care furnizează date câte un octet la un moment dat
  • Reader și Writer – învelișuri convenabile pentru fluxuri
  • modul de blocare – pentru a aștepta un mesaj complet

2.2. NIO – java.nio

Pachetul java.nio a fost introdus în Java 1.4 și actualizat în Java 1.7 (NIO.2) cu operații îmbunătățite cu fișiere și un canal ASynchronousSocketChannel. Acesta oferă:

  • Buffer – pentru a citi bucăți de date la un moment dat
  • CharsetDecoder – pentru cartografierea octeților neprelucrați în/din caractere lizibile
  • Channel – pentru comunicarea cu lumea exterioară
  • Selector – pentru a activa multiplexarea pe un SelectableChannel și a oferi acces la orice canal care este pregătit pentru I/O
  • non-blocking mode – pentru a citi tot ceea ce este gata

Acum, să vedem cum folosim fiecare dintre aceste pachete atunci când trimitem date către un server sau citim răspunsul acestuia.

Configurați serverul nostru de testare

Aici vom folosi WireMock pentru a simula un alt server, astfel încât să putem rula testele noastre în mod independent.

Îl vom configura pentru a asculta cererile noastre și pentru a ne trimite răspunsurile la fel cum ar face un server web real. De asemenea, vom folosi un port dinamic, astfel încât să nu intrăm în conflict cu nici un serviciu de pe mașina noastră locală.

Să adăugăm dependența Maven pentru WireMock cu domeniul de testare:

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

Într-o clasă de testare, să definim o @Rulă JUnit pentru a porni WireMock pe un port liber. Apoi îl vom configura pentru a ne returna un răspuns HTTP 200 atunci când cerem o resursă predefinită, cu corpul mesajului ca un text în format 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!\" }")));}

Acum că avem serverul nostru simulat configurat, suntem gata să rulăm câteva teste.

Blocking IO – java.io

Să vedem cum funcționează modelul original de blocaj IO prin citirea unor date de pe un site web. Vom folosi un java.net.Socket pentru a obține acces la unul dintre porturile sistemului de operare.

4.1. Trimiterea unei cereri

În acest exemplu, vom crea o cerere GET pentru a prelua resursele noastre. În primul rând, să creăm un Socket pentru a accesa portul pe care serverul nostru WireMock ascultă:

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

Pentru o comunicare normală HTTP sau HTTPS, portul ar fi 80 sau 443. Cu toate acestea, în acest caz, folosim wireMockRule.port() pentru a accesa portul dinamic pe care l-am configurat mai devreme.

Acum haideți să deschidem un OutputStream pe socket, înfășurat într-un OutputStreamWriter și să îl trecem la un PrintWriter pentru a scrie mesajul nostru. Și haideți să ne asigurăm că am curățat buffer-ul astfel încât cererea noastră să fie trimisă:

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. Așteptarea răspunsului

Să deschidem un InputStream pe socket pentru a accesa răspunsul, să citim fluxul cu un BufferedReader și să îl stocăm într-un StringBuilder:

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

Să folosim reader.readLine() pentru a bloca, așteptând o linie completă, apoi să adăugăm linia la magazinul nostru. Vom continua să citim până când vom obține un null, care indică sfârșitul fluxului:

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

Non-Blocking IO – java.nio

Acum, să vedem cum funcționează modelul IO non-blocking al pachetului nio cu același exemplu.

De data aceasta, vom crea un java.nio.channel.SocketChannel pentru a accesa portul de pe serverul nostru, în loc de un java.net.Socket, și îi vom trece o adresă InetSocketAddress.

5.1. Trimiteți o cerere

În primul rând, să deschidem SocketChannel-ul nostru:

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

Și acum, să obținem un set de caractere UTF-8 standard pentru a codifica și a scrie mesajul nostru:

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

5.2. Citirea răspunsului

După ce trimitem cererea, putem citi răspunsul în mod non-blocat, folosind tampoane brute.

Din moment ce vom procesa text, vom avea nevoie de un ByteBuffer pentru octeții brute și de un CharBuffer pentru caracterele convertite (ajutat de un CharsetDecoder):

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

CharBuffer-ul nostru va avea spațiu rămas dacă datele sunt trimise într-un set de caractere cu mai mulți octeți.

Rețineți că, dacă avem nevoie de performanțe deosebit de rapide, putem crea un MappedByteBuffer în memoria nativă folosind ByteBuffer.allocateDirect(). Cu toate acestea, în cazul nostru, utilizarea allocate() din heap-ul standard este suficient de rapidă.

Când avem de-a face cu tampoane, trebuie să știm cât de mare este bufferul (capacitatea), unde ne aflăm în buffer (poziția curentă) și cât de departe putem merge (limita).

Așa că, să citim de pe canalul nostru SocketChannel, trecându-i ByteBuffer-ul nostru pentru a stoca datele noastre. Citirea noastră de la SocketChannel se va termina cu poziția curentă a ByteBuffer-ului nostru setată la următorul octet în care se va scrie (imediat după ultimul octet scris), dar cu limita sa neschimbată:

socketChannel.read(byteBuffer)

Nostru SocketChannel.read() returnează numărul de octeți citiți care ar putea fi scriși în buffer-ul nostru. Acesta va fi -1 dacă socket-ul a fost deconectat.

Când buffer-ul nostru nu mai are spațiu pentru că nu am procesat încă toate datele sale, atunci SocketChannel.read() va returna zero octeți citiți, dar buffer-ul nostru.position() va fi în continuare mai mare decât zero.

Pentru a ne asigura că începem să citim din locul potrivit în buffer, vom folosi Buffer.flip() pentru a seta poziția curentă a ByteBuffer-ului nostru la zero și limita sa la ultimul octet care a fost scris de SocketChannel. Apoi vom salva conținutul bufferului folosind metoda storeBufferContents, pe care o vom analiza mai târziu. În cele din urmă, vom folosi buffer.compact() pentru a compacta bufferul și pentru a seta poziția curentă, gata pentru următoarea citire de la SocketChannel.

Din moment ce datele noastre pot sosi în părți, să înfășurăm codul nostru de citire a bufferului într-o buclă cu condiții de terminare pentru a verifica dacă socket-ul nostru este încă conectat sau dacă am fost deconectați, dar mai avem date în buffer:

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

Și să nu uităm să închidem() socket-ul nostru (cu excepția cazului în care l-am deschis într-un bloc try-with-resources):

socketChannel.close();

5.3. Stocarea datelor din bufferul nostru

Răspunsul de la server va conține anteturi, ceea ce poate face ca volumul de date să depășească dimensiunea bufferului nostru. Așadar, vom folosi un StringBuilder pentru a construi mesajul nostru complet pe măsură ce acesta sosește.

Pentru a stoca mesajul nostru, mai întâi decodificăm octeții brute în caractere în CharBuffer-ul nostru. Apoi vom întoarce indicatorii astfel încât să putem citi datele noastre de caractere și să le adăugăm la StringBuilder-ul nostru expandabil. În cele din urmă, vom șterge CharBuffer-ul, gata pentru următorul ciclu de scriere/citire.

Acum, să implementăm metoda noastră completă storeBufferContents(), trecând în tampoanele noastre, 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();}

Concluzie

În acest articol, am văzut cum metoda originală java.io se blochează, așteaptă o cerere și folosește Streams pentru a manipula datele pe care le primește.

În schimb, bibliotecile java.nio permit comunicarea fără blocaj folosind Buffers și Channels și pot oferi acces direct la memorie pentru o performanță mai rapidă. Cu toate acestea, odată cu această viteză vine și complexitatea suplimentară de a manipula bufferele.

Ca de obicei, codul pentru acest articol este disponibil pe GitHub.

Începeți să lucrați cu Spring 5 și Spring Boot 2, prin intermediul cursului Învățați Spring:

>>VEZI CURSUL

Lasă un răspuns

Adresa ta de email nu va fi publicată.