Panoramica

Gestire input e output sono compiti comuni per i programmatori Java. In questo tutorial, esamineremo le librerie originali java.io (IO) e le più recenti java.nio (NIO) e come differiscono quando comunicano attraverso una rete.

Caratteristiche chiave

Iniziamo guardando le caratteristiche chiave 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 dati un byte alla volta
  • Reader e Writer – wrapper di convenienza per i flussi
  • modalità bloccante – per aspettare 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 file migliorate e un ASynchronousSocketChannel. Esso fornisce:

  • Buffer – per leggere pezzi di dati alla volta
  • CharsetDecoder – per mappare i byte grezzi da e verso caratteri leggibili
  • Channel – per comunicare con il mondo esterno
  • Selector – per abilitare il multiplexing su un SelectableChannel e fornire accesso a qualsiasi canale che sia pronto per I/O
  • non-blocking mode – per leggere qualsiasi cosa sia pronta

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

Configuriamo il nostro server di test

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

Lo configureremo per ascoltare le nostre richieste e per inviarci le 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 di Maven per WireMock con lo scope di test:

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

In una classe di test, definiamo una @Ruola JUnit per avviare WireMock su una porta libera. Lo configureremo 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";@Beforepublic void setup() { stubFor(get(urlEqualTo(REQUESTED_RESOURCE)) .willReturn(aResponse() .withStatus(200) .withBody("{ \"response\" : \"It worked!\" }")));}

Ora che abbiamo impostato il nostro mock server, siamo pronti per eseguire alcuni test.

Blocking IO – java.io

Guardiamo come funziona il modello originale di IO bloccante leggendo alcuni dati da un sito web. Useremo un java.net.Socket per accedere a una delle porte del sistema operativo.

4.1. Inviare una richiesta

In questo esempio, creeremo una richiesta GET per recuperare le nostre risorse. Per prima cosa, creiamo un Socket per accedere alla porta che il nostro server WireMock sta ascoltando:

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

Per una 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 lavare 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. Wait for the Response

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();

Utilizziamo reader.readLine() per bloccare, aspettando una riga completa, poi aggiungiamo la riga al nostro store. Continueremo a leggere finché non otterremo un null, che indica la fine del flusso:

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

Io non bloccante – java.nio

Ora, vediamo 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 un java.net.Socket, e gli passeremo un InetSocketAddress.

5.1. Send a Request

Prima, apriamo il nostro SocketChannel:

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

E ora, prendiamo un set di caratteri UTF-8 standard 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. Leggere la risposta

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

Siccome elaboreremo del testo, avremo bisogno di un ByteBuffer per i byte grezzi e di un CharBuffer per i caratteri convertiti (aiutati 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 sono inviati in un set di caratteri multi-byte.

Nota che se abbiamo bisogno di prestazioni particolarmente veloci, possiamo creare un MappedByteBuffer in memoria nativa usando ByteBuffer.allocateDirect(). Tuttavia, nel nostro caso, usare allocate() dall’heap standard è abbastanza veloce.

Quando abbiamo a che fare con i buffer, abbiamo bisogno di sapere quanto è grande il buffer (la capacità), dove siamo nel buffer (la posizione attuale), e quanto lontano possiamo andare (il limite).

Quindi, leggiamo dal nostro SocketChannel, passandogli il nostro ByteBuffer per memorizzare i nostri dati. La nostra lettura dal SocketChannel terminerà con la posizione corrente del nostro ByteBuffer impostata sul prossimo byte da scrivere (subito dopo l’ultimo byte scritto), ma con il suo limite invariato:

socketChannel.read(byteBuffer)

Il nostro SocketChannel.read() restituisce il numero di byte letti che potrebbero essere scritti nel nostro buffer. Questo sarà -1 se il socket è stato disconnesso.

Quando il nostro buffer non ha più spazio perché non abbiamo ancora elaborato tutti i suoi dati, allora SocketChannel.read() restituirà zero byte letti ma il nostro buffer.position() sarà ancora maggiore di zero.

Per essere sicuri di iniziare a leggere dal posto giusto nel buffer, useremo Buffer.flip() per impostare la posizione corrente del nostro ByteBuffer a zero e il suo limite all’ultimo byte che è stato scritto dal SocketChannel. Salveremo poi il contenuto del buffer usando il nostro metodo storeBufferContents, che vedremo in seguito. Infine, useremo buffer.compact() per compattare il buffer e impostare la posizione corrente pronta per la nostra prossima lettura dal SocketChannel.

Siccome i nostri dati possono arrivare in parti, avvolgiamo il nostro codice di lettura del buffer in un ciclo con condizioni di terminazione per controllare se il nostro socket è ancora connesso o se siamo stati disconnessi ma abbiamo ancora dati nel nostro buffer:

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

E non dimentichiamo di chiudere() il nostro socket (a meno che non lo abbiamo aperto in un blocco try-with-resources):

socketChannel.close();

5.3. Memorizzare i dati dal nostro buffer

La risposta del server conterrà delle intestazioni, che possono far sì che la quantità di dati superi la dimensione del nostro buffer. Quindi, useremo uno StringBuilder per costruire il nostro messaggio completo quando arriva.

Per memorizzare il nostro messaggio, prima decodifichiamo i byte grezzi in caratteri nel nostro CharBuffer. Poi invertiremo i puntatori in modo da poter leggere i dati dei caratteri e li aggiungeremo al nostro StringBuilder espandibile. Infine, cancelleremo il CharBuffer pronto per il prossimo ciclo di scrittura/lettura.

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();}

Conclusione

In questo articolo, abbiamo visto come il modello originale java.io si blocchi, attenda una richiesta e usi gli stream per manipolare i dati che riceve.

Al contrario, le librerie java.nio permettono una comunicazione non bloccante usando buffer e canali e possono fornire un accesso diretto alla memoria per prestazioni più veloci. Tuttavia, con questa velocità arriva l’ulteriore complessità della gestione dei buffer.

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

Inizia con Spring 5 e Spring Boot 2, attraverso il corso Learn Spring:

>> CHECK OUT THE COURSE

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.