CodeGym /Cours /JAVA 25 SELF /NIO Channels et ByteBuffer

NIO Channels et ByteBuffer

JAVA 25 SELF
Niveau 41 , Leçon 1
Disponible

1. Introduction aux canaux NIO

Dans l’IO classique de Java (java.io), tout est organisé selon le principe « un thread — un fichier ou une ressource ». Dès que la lecture ou l’écriture commence, le thread est bloqué et attend la fin de l’opération. Pour les cas simples, c’est pratique, mais dans des systèmes très sollicités cette approche devient un goulot d’étranglement : s’il y a des milliers de connexions, alors des milliers de threads restent occupés à attendre.

Avec NIO (New IO), l’approche est différente. Ici, les E/S peuvent être non bloquantes et un thread n’est pas obligé d’être inactif. Tant que certaines données sont encore en transit, il peut se basculer sur une autre tâche. Cela permet de servir un très grand nombre de connexions avec seulement quelques threads.

La différence se voit aussi dans les détails. Dans l’« ancien » IO, le travail s’articule autour des flux qui lisent et écrivent des octets ou des caractères, mais se bloquent toujours pendant les opérations. En NIO, les notions clés sont les canaux (Channels) et les tampons (Buffers). Elles permettent de réaliser des E/S non bloquantes (important pour les serveurs), ainsi que d’utiliser la technique zero-copy, lorsque les données sont transférées directement, en évitant des copies supplémentaires dans les tampons de la JVM.

Comparaison : flux (Streams) vs canaux (Channels)

Flux (InputStream/OutputStream) :

  • Lisent/écrivent des octets un par un ou par tableaux.
  • Pas de contrôle direct de la position dans le fichier.
  • Efficacité limitée avec des fichiers très volumineux.

Canaux (Channel) :

  • Lisent/écrivent les données via des tampons (Buffer).
  • Possibilité de gérer la position (accès aléatoire inclus).
  • Prise en charge de l’asynchronisme et du mode non bloquant.
  • Permettent d’utiliser le zero-copy pour des copies ultrarapides.

2. FileChannel et SeekableByteChannel

Lecture et écriture de données à l’aide de tampons

FileChannel est le canal principal pour travailler avec des fichiers. On peut l’obtenir à partir de FileInputStream, FileOutputStream ou via NIO.2 — Files.newByteChannel (renvoie SeekableByteChannel).

Exemple : lecture d’un fichier via FileChannel et ByteBuffer

import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileChannelReadExample {
    public static void main(String[] args) throws Exception {
        try (RandomAccessFile file = new RandomAccessFile("data.txt", "r");
             FileChannel channel = file.getChannel()) {

            ByteBuffer buffer = ByteBuffer.allocate(1024); // tampon de 1 Ko

            int bytesRead = channel.read(buffer); // lecture dans le tampon
            while (bytesRead != -1) {
                buffer.flip(); // passer le tampon en mode lecture
                while (buffer.hasRemaining()) {
                    System.out.print((char) buffer.get());
                }
                buffer.clear(); // nettoyer le tampon pour la lecture suivante
                bytesRead = channel.read(buffer);
            }
        }
    }
}

Écriture dans un fichier :

try (RandomAccessFile file = new RandomAccessFile("output.txt", "rw");
     FileChannel channel = file.getChannel()) {

    ByteBuffer buffer = ByteBuffer.wrap("Hello, NIO!\n".getBytes());
    channel.write(buffer);
}

NIO.2 : ouverture d’un canal via Files.newByteChannel

import java.nio.file.*;
import java.nio.channels.SeekableByteChannel;
import static java.nio.file.StandardOpenOption.*;

Path path = Paths.get("data.txt");
try (SeekableByteChannel ch = Files.newByteChannel(path, READ)) {
    ByteBuffer buf = ByteBuffer.allocate(256);
    ch.read(buf);
}

Positionnement (position()) et modification de taille (truncate())

  • position() — permet de lire ou de fixer la position actuelle dans le fichier (analogue du « curseur »).
  • truncate(long size) — tronque le fichier à la taille indiquée.
channel.position(100);   // se déplacer au 100e octet
channel.truncate(1024);  // tronquer le fichier à 1 Ko

Accès direct et positionnel aux fichiers

  • Accès direct : on peut lire/écrire à n’importe quel endroit du fichier, pas seulement de façon séquentielle.
  • Accès positionnel : on peut lire/écrire des données à une position précise sans changer la position courante du canal.
ByteBuffer buffer = ByteBuffer.allocate(4);
channel.read(buffer, 128);   // lire 4 octets à partir de la position 128 sans modifier channel.position()

3. ByteBuffer : comment cela fonctionne

Paramètres principaux : capacity, limit, position, mark

  • capacity — taille maximale du tampon (définie à la création).
  • limit — borne jusqu’à laquelle on peut lire/écrire (par défaut, égale à capacity).
  • position — position courante (où l’on écrit/d’où l’on lit).
  • mark — « marque » que l’on peut fixer puis retrouver.

Cycle de vie d’un tampon :

  1. On écrit des données dans le tampon (par exemple, via read() depuis un canal).
  2. flip() — on bascule le tampon en mode lecture (position = 0, limit = position actuelle).
  3. On lit les données du tampon (get()).
  4. clear() — on nettoie le tampon pour la prochaine écriture (position = 0, limit = capacity).

Exemple :

ByteBuffer buffer = ByteBuffer.allocate(8);
buffer.put((byte) 42);
buffer.flip();                // on peut lire maintenant
byte value = buffer.get();    // 42
buffer.clear();               // prêt pour un nouvel enregistrement

Création de tampons : allocate() vs allocateDirect()

Le ByteBuffer propose deux façons principales de créer un tampon, et la différence entre elles se ressent à l’usage. La méthode allocate() place le tampon dans le tas de la JVM : création rapide et adaptée à la plupart des cas, mais lors des E/S natives, des copies supplémentaires entre le tas et la mémoire de l’OS peuvent survenir.

La méthode allocateDirect() alloue de la mémoire en dehors du tas de la JVM (en « native memory »). Un tel tampon est plus coûteux à créer et plus complexe à gérer, mais pour la lecture/écriture de gros fichiers ou les opérations réseau il est souvent plus rapide grâce à l’absence de copies superflues.

L’idée est simple : si la performance sur de gros volumes est cruciale — utilisez des tampons « directs ». Pour des opérations petites et fréquentes, le surcoût de création peut dépasser le bénéfice.

ByteBuffer directBuffer = ByteBuffer.allocateDirect(4096);

4. Opérations haute performance : transferTo() et transferFrom()

Méthodes transferTo() et transferFrom()

La classe FileChannel propose deux méthodes qui permettent de travailler selon le principe du « zéro copie » — transferTo() et transferFrom(). L’idée est de transférer les données directement entre canaux de fichiers ou, par exemple, entre un fichier et le réseau. La JVM intervient très peu : l’opération est réalisée par l’OS et les tampons au sein de Java ne sont pas manipulés.

Résultat : la copie de gros fichiers fonctionne nettement plus vite : moins de copies, moins de bascules entre l’espace utilisateur et le noyau, et une charge CPU plus faible.

Exemple : copie de fichier via zero-copy

import java.nio.channels.FileChannel;
import java.nio.file.*;

public class ZeroCopyExample {
    public static void main(String[] args) throws Exception {
        try (FileChannel src = FileChannel.open(Paths.get("input.bin"), StandardOpenOption.READ);
             FileChannel dst = FileChannel.open(Paths.get("output.bin"), StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {

            long size = src.size();
            long transferred = src.transferTo(0, size, dst);
            System.out.println("Octets copiés : " + transferred);
        }
    }
}

Quand le zero-copy fonctionne-t-il réellement ?

  • Lors de la copie entre des fichiers situés sur le même disque.
  • Lors de l’envoi de fichiers sur le réseau (par exemple via SocketChannel).
  • Lorsque l’OS prend en charge le zero-copy (Linux, macOS, Windows — le prennent en charge).

Avantages :

  • Copie minimale : les données ne traversent pas les tampons de la JVM.
  • Grande vitesse : moins de bascules et une charge CPU réduite.
  • Moins de mémoire : pas besoin de grands tampons côté utilisateur.

Exemple : copie de fichier « en une seule ligne »

Files.copy(Paths.get("input.bin"), Paths.get("output.bin"), StandardCopyOption.REPLACE_EXISTING);
// Peut utiliser le zero-copy en interne si possible

5. Erreurs courantes

Erreur №1 : oublier flip() avant de lire depuis le tampon. Après avoir écrit dans le tampon, appelez impérativement flip(), sinon la lecture ne fonctionnera pas comme prévu : position/limit resteront en « mode écriture ».

Erreur №2 : utiliser allocateDirect() pour de petites opérations. Les tampons directs sont excellents pour de gros volumes, mais pour de petites requêtes, leur création est inutilement coûteuse. Par défaut, privilégiez allocate().

Erreur №3 : oublier de fermer le canal. Utilisez toujours try-with-resources pour les canaux et les flux afin d’éviter les fuites de descripteurs.

Erreur №4 : confondre position/limit/capacity. Avant lecture/écriture, assurez-vous du mode du tampon : après une écriture, il faut flip(), après une lecture, pour une nouvelle écriture — clear() ou compact().

Erreur №5 : penser que le zero-copy « fonctionne toujours ». Sur certaines configurations (autres périphériques/systèmes de fichiers différents/options particulières), le zero-copy peut être indisponible — un simple copier-coller aura alors lieu, et la performance sera différente.

1
Mission
JAVA 25 SELF, niveau 41, leçon 1
Bloqué
Chirurgie des données : Sculpture du rouleau binaire ✂️
Chirurgie des données : Sculpture du rouleau binaire ✂️
1
Mission
JAVA 25 SELF, niveau 41, leçon 1
Bloqué
Coursier numérique : Livraison instantanée de copies 📦
Coursier numérique : Livraison instantanée de copies 📦
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION