- Overview
- Fonctionnalités clés
- 2.1. IO – java.io
- 2.2. NIO – java.nio
- Configurer notre serveur de test
- Blocking IO – java.io
- 4.1. Envoyer une requête
- 4.2. Attendre la réponse
- Non-Blocking IO – java.nio
- 5.1. Envoyer une requête
- 5.2. Lire la réponse
- 5.3. Stocker les données de notre tampon
- Conclusion
- Démarrez avec Spring 5 et Spring Boot 2, grâce au cours Learn Spring:
Overview
Gérer les entrées et les sorties sont des tâches courantes pour les programmeurs Java. Dans ce tutoriel, nous examinerons les bibliothèques originales java.io (IO) et les bibliothèques plus récentes java.nio (NIO) et comment elles diffèrent lors de la communication à travers un réseau.
Fonctionnalités clés
Débutons en regardant les fonctionnalités clés des deux paquets.
2.1. IO – java.io
Le paquet java.io a été introduit dans Java 1.0, avec Reader introduit dans Java 1.1. Il fournit :
- InputStream et OutputStream – qui fournissent des données un octet à la fois
- Reader et Writer – enveloppes de commodité pour les flux
- mode bloquant – pour attendre un message complet
2.2. NIO – java.nio
Le paquet 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 fournit :
- Tampon – pour lire des morceaux de données à la fois
- CharsetDecoder – pour mapper les octets bruts vers/depuis des caractères lisibles
- Canal – pour communiquer avec le monde extérieur
- Sélecteur – pour activer le multiplexage sur un SelectableChannel et fournir un accès à tous les canaux qui sont prêts pour les E/S
- non-.blocking mode – pour lire tout ce qui est prêt
Maintenant, regardons comment nous utilisons chacun de ces paquets lorsque nous envoyons des données à un serveur ou lisons sa réponse.
Configurer notre serveur de test
Nous utiliserons ici WireMock pour simuler un autre serveur afin de pouvoir exécuter nos tests indépendamment.
Nous le configurerons pour écouter nos requêtes 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 une portée de test :
<dependency> <groupId>com.github.tomakehurst</groupId> <artifactId>wiremock-jre8</artifactId> <version>2.26.3</version> <scope>test</scope></dependency>
Dans une classe de test, définissons une @Rule JUnit pour démarrer WireMock sur un port libre. Nous le configurerons ensuite pour qu’il nous renvoie une réponse HTTP 200 lorsque nous demandons une ressource prédéfinie, avec le corps du message comme un certain texte au 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!\" }")));}
Maintenant que nous avons notre serveur fantaisie configuré, nous sommes prêts à exécuter quelques tests.
Blocking IO – java.io
Regardons comment le modèle original de blocking IO fonctionne en lisant quelques données à partir d’un site web. Nous utiliserons un java.net.Socket pour avoir accès à l’un des ports du système d’exploitation.
4.1. Envoyer une requête
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. Attendre 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());}
Non-Blocking IO – java.nio
Maintenant, regardons comment le modèle d’IO non-bloquant du paquet nio fonctionne avec le même exemple.
Cette fois, nous allons créer un java.nio.channel.SocketChannel pour accéder au port de notre serveur au lieu d’un java.net.Socket, et lui passer une InetSocketAddress.
5.1. Envoyer une requête
D’abord, ouvrons notre SocketChannel:
InetSocketAddress address = new InetSocketAddress("localhost", wireMockRule.port());SocketChannel socketChannel = SocketChannel.open(address);
Et maintenant, obtenons un jeu de caractères UTF-8 standard pour coder 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. Lire 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 allons traiter du texte, nous aurons besoin d’un ByteBuffer pour les octets bruts et d’un CharBuffer pour les caractères convertis (aidés par 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.
Notez que si nous avons besoin de performances particulièrement rapides, nous pouvons créer un MappedByteBuffer en mémoire native en utilisant ByteBuffer.allocateDirect(). Cependant, dans notre cas, l’utilisation de allocate() à partir du tas standard est assez rapide.
Lorsque nous traitons avec des tampons, nous devons savoir quelle est la taille du tampon (la capacité), où nous sommes dans le tampon (la position actuelle), et jusqu’où nous pouvons aller (la limite).
Donc, lisons depuis notre SocketChannel, en lui passant notre ByteBuffer pour stocker nos données. Notre lecture depuis le SocketChannel se terminera avec la position actuelle de notre ByteBuffer définie sur le prochain octet à écrire (juste après le dernier octet écrit), mais avec sa limite inchangée :
socketChannel.read(byteBuffer)
Notre SocketChannel.read() renvoie le nombre d’octets lus qui pourraient être écrits dans notre tampon. Ce sera -1 si la socket a été déconnectée.
Lorsque notre tampon n’a plus d’espace parce que nous n’avons pas encore traité toutes ses données, alors SocketChannel.read() retournera zéro octet lu mais notre buffer.position() sera toujours supérieur à zéro.
Pour nous assurer que nous commençons à lire au bon endroit dans le tampon, nous utiliserons Buffer.flip() pour mettre la position actuelle de notre ByteBuffer à zéro et sa limite au dernier octet qui a été écrit par le SocketChannel. Nous sauvegarderons ensuite le contenu du tampon à l’aide de la méthode storeBufferContents, que nous verrons plus tard. Enfin, nous utiliserons buffer.compact() pour compacter le tampon et définir la position actuelle prête pour notre prochaine lecture depuis le SocketChannel.
Puisque nos données peuvent arriver par parties, enveloppons notre code de lecture de tampon dans une boucle avec des conditions de terminaison pour vérifier si notre socket est toujours connecté ou si nous avons été déconnectés mais qu’il reste encore des données dans notre tampon :
while (socketChannel.read(byteBuffer) != -1 || byteBuffer.position() > 0) { byteBuffer.flip(); storeBufferContents(byteBuffer, charBuffer, charsetDecoder, ourStore); byteBuffer.compact();}
Et n’oublions pas de fermer() notre socket (sauf si nous l’avons ouvert dans un bloc try-with-resources) :
socketChannel.close();
5.3. Stocker les données de notre tampon
La réponse du serveur contiendra des en-têtes, ce qui peut faire que la quantité de données dépasse la taille de notre tampon. Nous utiliserons donc un StringBuilder pour construire notre message complet au fur et à mesure de son arrivée.
Pour stocker notre message, nous décodons d’abord les octets bruts en caractères dans notre CharBuffer. Ensuite, nous retournerons les pointeurs afin de pouvoir lire nos données de caractères, et les ajouterons à notre StringBuilder extensible. Enfin, nous effacerons le CharBuffer prêt pour le prochain cycle d’écriture/lecture.
Alors maintenant, implémentons notre méthode complète storeBufferContents() en passant dans nos buffers, 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();}
Conclusion
Dans cet article, nous avons vu comment le modèle original java.io se bloque, attend une requête et utilise des Streams pour manipuler les données qu’il reçoit.
En revanche, les bibliothèques java.nio permettent une communication non bloquante en utilisant des Buffers et des Channels et peuvent fournir un accès direct à la mémoire pour des performances plus rapides. Cependant, cette vitesse s’accompagne d’une complexité supplémentaire liée à la manipulation des tampons.
Comme d’habitude, le code de cet article est disponible sur GitHub.
Démarrez avec Spring 5 et Spring Boot 2, grâce au cours Learn Spring:
>> VÉRIFIER LE COURS
.