Java IO vs NIO

1. Vue d'ensemble

La gestion des entrées et des sorties est des tâches courantes pour les programmeurs Java. Dans ce tutoriel, nous allons voir l' origine java.io bibliothèques (IO) et les plus récentes java.nio bibliothèques (NIO) et comment ils diffèrent lors de la communication à travers un réseau.

2. Principales caractéristiques

Commençons par examiner les principales fonctionnalités des deux packages.

2.1. IO - java.io

Le package java.io a été introduit dans Java 1.0 , avec Reader introduit dans Java 1.1. Il offre:

  • InputStream et OutputStream - qui fournissent des données un octet à la fois
  • Reader and Writer - wrappers pratiques pour les flux
  • mode de blocage - pour attendre un message complet

2.2. NIO - java.nio

Le package java.nio a été introduit dans Java 1.4 et mis à jour dans Java 1.7 (NIO.2) avec des opérations de fichiers améliorées et un ASynchronousSocketChannel . Il offre:

  • Buffer - pour lire des morceaux de données à la fois
  • CharsetDecoder - pour mapper des octets bruts vers / à partir de caractères lisibles
  • Canal - pour communiquer avec le monde extérieur
  • Sélecteur - pour activer le multiplexage sur un SelectableChannel et donner accès à tous les canaux prêts pour les E / S
  • mode non bloquant - pour lire ce qui est prêt

Voyons maintenant comment nous utilisons chacun de ces packages lorsque nous envoyons des données à un serveur ou lisons sa réponse.

3. Configurez notre serveur de test

Ici, nous allons utiliser WireMock pour simuler un autre serveur afin que nous puissions exécuter nos tests indépendamment.

Nous le configurerons pour écouter nos demandes et nous envoyer des réponses comme le ferait un vrai serveur Web. Nous utiliserons également un port dynamique afin de ne pas entrer en conflit avec les services de notre machine locale.

Ajoutons la dépendance Maven pour WireMock avec la portée de test :

 com.github.tomakehurst wiremock-jre8 2.26.3 test 

Dans une classe de test, définissons une JUnit @Rule pour démarrer WireMock sur un port libre. Nous le configurerons ensuite pour nous renvoyer une réponse HTTP 200 lorsque nous demandons une ressource prédéfinie, avec le corps du message sous forme de texte au format 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!\" }"))); }

Maintenant que notre serveur simulé est configuré, nous sommes prêts à exécuter des tests.

4. Blocage d'E / S - java.io

Voyons comment fonctionne le modèle IO de blocage d'origine en lisant certaines données d'un site Web. Nous utiliserons un java.net.Socket pour accéder à l'un des ports du système d'exploitation.

4.1. Envoyer une demande

Dans cet exemple, nous allons créer une requête GET pour récupérer nos ressources. Tout d'abord, créons un Socket pour accéder au port sur lequel notre serveur WireMock écoute:

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

Pour une communication HTTP ou HTTPS normale, le port serait 80 ou 443. Cependant, dans ce cas, nous utilisons wireMockRule.port () pour accéder au port dynamique que nous avons configuré précédemment.

Maintenant, ouvrons un OutputStream sur le socket , enveloppé dans un OutputStreamWriter et passons-le à un PrintWriter pour écrire notre message. Et assurons-nous de vider le tampon pour que notre requête soit envoyée:

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. Attendez la réponse

Ouvrons un InputStream sur le socket pour accéder à la réponse, lisons le flux avec un BufferedReader et stockons-le dans un StringBuilder :

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

Utilisons reader.readLine () pour bloquer, en attendant une ligne complète, puis ajoutons la ligne à notre magasin. Nous continuerons à lire jusqu'à ce que nous obtenions un null, qui indique la fin du flux:

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

5. E / S non bloquantes - java.nio

Voyons maintenant comment le modèle d'E / S non bloquant du package nio fonctionne avec le même exemple.

Cette fois, nous allons créer un java.nio.channel . SocketChannel pour accéder au port sur notre serveur au lieu d'un java.net.Socket , et lui passer un InetSocketAddress .

5.1. Envoyer une demande

Tout d'abord, ouvrons notre SocketChannel :

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

Et maintenant, nous allons obtenir une norme UTF-8 Charset pour encoder et écrire notre message:

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

5.2. Lisez la réponse

Après avoir envoyé la requête, nous pouvons lire la réponse en mode non bloquant, en utilisant des tampons bruts.

Puisque nous traiterons du texte, nous aurons besoin d'un ByteBuffer pour les octets bruts et d'un CharBuffer pour les caractères convertis (avec l'aide d'un CharsetDecoder ):

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

Notre CharBuffer aura de l'espace restant si les données sont envoyées dans un jeu de caractères multi-octets.

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.

Alors maintenant, implémentons notre méthode storeBufferContents () complète en passant nos tampons, CharsetDecoder et StringBuilder :

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

6. Conclusion

Dans cet article, nous avons vu comment le modèle java.io d' origine se bloque , attend une requête et utilise Stream s pour manipuler les données qu'il reçoit.

En revanche, les bibliothèques java.nio permettent une communication non bloquante à l' aide des tampons et des canaux et peuvent fournir un accès direct à la mémoire pour des performances plus rapides. Cependant, avec cette vitesse vient la complexité supplémentaire de la gestion des tampons.

Comme d'habitude, le code de cet article est disponible à l'adresse over sur GitHub.