Java IO contro NIO

1. Panoramica

La gestione dell'input e dell'output sono attività comuni per i programmatori Java. In questo tutorial, vedremo l' originale java.io (IO) le biblioteche e il più recente java.nio librerie (NIO) e come si differenziano quando si comunica attraverso una rete.

2. Caratteristiche principali

Cominciamo esaminando le caratteristiche principali di entrambi i pacchetti.

2.1. IO - java.io

Il pacchetto java.io è stato introdotto in Java 1.0 , con Reader introdotto in Java 1.1. Fornisce:

  • InputStream e OutputStream - che forniscono i dati un byte alla volta
  • Reader e Writer : vantaggiosi wrapper per i flussi
  • modalità di blocco - per attendere un messaggio completo

2.2. NIO - java.nio

Il pacchetto java.nio è stato introdotto in Java 1.4 e aggiornato in Java 1.7 (NIO.2) con operazioni sui file migliorate e un ASynchronousSocketChannel . Fornisce:

  • Buffer : per leggere blocchi di dati alla volta
  • CharsetDecoder - per mappare byte grezzi a / da caratteri leggibili
  • Canale - per comunicare con il mondo esterno
  • Selector - per abilitare il multiplexing su un SelectableChannel e fornire l'accesso a qualsiasi canale pronto per I / O
  • modalità non bloccante - per leggere tutto ciò che è pronto

Ora diamo un'occhiata a come usiamo ciascuno di questi pacchetti quando inviamo dati a un server o leggiamo la sua risposta.

3. Configurare il nostro server di prova

Qui utilizzeremo WireMock per simulare un altro server in modo da poter eseguire i nostri test in modo indipendente.

Lo configureremo per ascoltare le nostre richieste e per inviarci risposte proprio come farebbe un vero server web. Useremo anche una porta dinamica in modo da non entrare in conflitto con nessun servizio sulla nostra macchina locale.

Aggiungiamo la dipendenza Maven per WireMock con l' ambito del test :

 com.github.tomakehurst wiremock-jre8 2.26.3 test 

In una classe di test, definiamo una JUnit @Rule per avviare WireMock su una porta libera. Lo configureremo quindi per restituirci una risposta HTTP 200 quando chiediamo una risorsa predefinita, con il corpo del messaggio come testo in formato JSON:

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

Ora che abbiamo configurato il nostro server fittizio, siamo pronti per eseguire alcuni test.

4. Blocco IO - java.io

Diamo un'occhiata a come funziona il modello I / O di blocco originale leggendo alcuni dati da un sito web. Useremo java.net.Socket per ottenere l'accesso a una delle porte del sistema operativo.

4.1. Invia una richiesta

In questo esempio, creeremo una richiesta GET per recuperare le nostre risorse. Per prima cosa, creiamo un Socket per accedere alla porta su cui è in ascolto il nostro server WireMock:

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

Per la normale comunicazione HTTP o HTTPS, la porta sarebbe 80 o 443. Tuttavia, in questo caso, usiamo wireMockRule.port () per accedere alla porta dinamica che abbiamo impostato in precedenza.

Ora apriamo un OutputStream sul socket , avvolto in un OutputStreamWriter e passiamolo a un PrintWriter per scrivere il nostro messaggio. E assicuriamoci di svuotare il buffer in modo che la nostra richiesta venga inviata:

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. Aspetta la risposta

Apriamo un InputStream sul socket per accedere alla risposta, leggiamo il flusso con un BufferedReader e memorizziamolo in uno StringBuilder :

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

Usiamo reader.readLine () per bloccare, aspettando una riga completa, quindi aggiungiamo la riga al nostro negozio. Continueremo a leggere fino a ottenere un valore nullo, che indica la fine del flusso:

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

5. IO non bloccante - java.nio

Ora, diamo un'occhiata a come funziona il modello IO non bloccante del pacchetto nio con lo stesso esempio.

Questa volta creeremo un java.nio.channel . SocketChannel per accedere alla porta sul nostro server invece di java.net.Socket e passargli un InetSocketAddress .

5.1. Invia una richiesta

Per prima cosa, apriamo il nostro SocketChannel :

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

E ora, veniamo uno standard UTF-8 Charset per codificare e scrivere il nostro messaggio:

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

5.2. Leggi la risposta

Dopo aver inviato la richiesta, possiamo leggere la risposta in modalità non bloccante, utilizzando buffer non elaborati.

Dato che elaboreremo il testo, avremo bisogno di un ByteBuffer per i byte non elaborati e di un CharBuffer per i caratteri convertiti (aiutato da un CharsetDecoder ):

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

Il nostro CharBuffer avrà spazio residuo se i dati vengono inviati in un set di caratteri multibyte.

Note that if we need especially fast performance, we can create a MappedByteBuffer in native memory using ByteBuffer.allocateDirect(). However, in our case, using allocate() from the standard heap is fast enough.

When dealing with buffers, we need to know how big the buffer is (the capacity), where we are in the buffer (the current position), and how far we can go (the limit).

So, let's read from our SocketChannel, passing it our ByteBuffer to store our data. Our read from the SocketChannel will finish with our ByteBuffer‘s current position set to the next byte to write to (just after the last byte written), but with its limit unchanged:

socketChannel.read(byteBuffer)

Our SocketChannel.read() returns the number of bytes read that could be written into our buffer. This will be -1 if the socket was disconnected.

When our buffer doesn't have any space left because we haven't processed all its data yet, then SocketChannel.read() will return zero bytes read but our buffer.position() will still be greater than zero.

To make sure that we start reading from the right place in the buffer, we'll use Buffer.flip() to set our ByteBuffer‘s current position to zero and its limit to the last byte that was written by the SocketChannel. We'll then save the buffer contents using our storeBufferContents method, which we'll look at later. Lastly, we'll use buffer.compact() to compact the buffer and set the current position ready for our next read from the SocketChannel.

Since our data may arrive in parts, let's wrap our buffer-reading code in a loop with termination conditions to check if our socket is still connected or if we've been disconnected but still have data left in our buffer:

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

And let's not forget to close() our socket (unless we opened it in a try-with-resources block):

socketChannel.close();

5.3. Storing Data From Our Buffer

The response from the server will contain headers, which may make the amount of data exceed the size of our buffer. So, we'll use a StringBuilder to build our complete message as it arrives.

To store our message, we first decode the raw bytes into characters in our CharBuffer. Then we'll flip the pointers so that we can read our character data, and append it to our expandable StringBuilder. Lastly, we'll clear the CharBuffer ready for the next write/read cycle.

Quindi ora, implementiamo il nostro metodo completo storeBufferContents () passando nei nostri buffer, CharsetDecoder e StringBuilder :

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

6. Conclusione

In questo articolo, abbiamo visto come il modello originale java.io si blocca , attende una richiesta e utilizza Stream s per manipolare i dati che riceve.

Al contrario, le librerie java.nio consentono la comunicazione non bloccante utilizzando i buffer e i canali e possono fornire un accesso diretto alla memoria per prestazioni più veloci. Tuttavia, con questa velocità arriva la complessità aggiuntiva della gestione dei buffer.

Come al solito, il codice per questo articolo è disponibile su GitHub.