Java NIO offre une manière différente de travailler les E/S par rapport aux E/S standard :
Canaux et tampons : IO standard fonctionne sur la base de flux d'octets et de flux de caractères, tandis que NIO fonctionne sur la base de canaux (canal) et de tampons (buffer). Les données sont toujours lues du canal vers la zone tampon ou écrites du tampon vers la zone tampon. canal.
IO asynchrone : Java NIO vous permet d'utiliser IO de manière asynchrone. Par exemple, lorsqu'un thread lit les données d'un canal dans un tampon, le thread peut toujours faire autre chose. Lorsque les données sont écrites dans le tampon, le thread peut continuer à les traiter. L'écriture sur un canal à partir d'un tampon est similaire.
Sélecteurs : Java NIO introduit le concept de sélecteurs, qui permettent d'écouter des événements sur plusieurs canaux (par exemple : ouverture de connexion, arrivée de données). Par conséquent, un seul thread peut écouter plusieurs canaux de données.
Présentons en détail les connaissances pertinentes de Java NIO.
Présentation de Java NIO
Java NIO se compose des éléments principaux suivants :
Canaux
Tampons
Sélecteurs
Bien qu'il existe de nombreuses autres classes et composants dans Java NIO, à mon avis, Channel, Buffer et Selector constituent l'API principale. D'autres composants, tels que Pipe et FileLock, ne sont que des classes utilitaires utilisées avec les trois composants principaux. Par conséquent, dans cet aperçu, je me concentrerai sur ces trois composants. D'autres composants sont traités dans des chapitres distincts.
Canal et tampon
Fondamentalement, toutes les E/S dans NIO démarrent à partir d'un canal. Les chaînes sont un peu comme les flux. Les données peuvent être lues du canal vers le tampon ou écrites du tampon vers le canal. Voici une illustration :
Il existe plusieurs types de canaux et de tampons. Voici les implémentations de certains canaux principaux dans JAVA NIO :
Canal de fichiers
DatagrammeChannel
SocketChannel
ServeurSocketChannel
Comme vous pouvez le voir, ces canaux couvrent les E/S des réseaux UDP et TCP, ainsi que les E/S des fichiers.
Parallèlement à ces classes, il existe des interfaces intéressantes, mais par souci de simplicité, j'ai essayé de ne pas les mentionner dans la présentation. Je les expliquerai dans d'autres chapitres de ce tutoriel où ils sont pertinents.
Voici la principale implémentation de Buffer dans Java NIO :
OctetBuffer
CharBuffer
DoubleBuffer
Tampon flottant
IntBuffer
Tampon Long
Tampon court
Ces tampons couvrent les types de données de base que vous pouvez envoyer via IO : byte, short, int, long, float, double et char.
Java NIO possède également un Mappyteuffer, qui est utilisé pour représenter les fichiers mappés en mémoire. Je ne vais pas l'expliquer dans la présentation.
Sélecteur
Le sélecteur permet à un seul thread de gérer plusieurs canaux. Si votre application ouvre plusieurs connexions (canaux), mais que le trafic de chaque connexion est très faible, l'utilisation de Selector peut s'avérer pratique. Par exemple, dans un serveur de chat.
Ceci est une illustration de l'utilisation d'un sélecteur pour traiter 3 canaux dans un seul thread :
Pour utiliser un sélecteur, vous devez enregistrer un canal auprès du sélecteur, puis appeler sa méthode select(). Cette méthode sera bloquée jusqu'à ce qu'une chaîne enregistrée ait un événement prêt. Une fois cette méthode renvoyée, le thread peut gérer ces événements. Des exemples d'événements sont l'arrivée de nouvelles connexions, la réception de données, etc.
Java NIO contre IO
(Adresse originale de cette partie, auteur : Jakob Jenkov, traducteur : Guo Lei, correcteur : Fang Tengfei)
Après avoir découvert Java NIO et l'API IO, une question m'est immédiatement venue à l'esprit :
Citation
Quand dois-je utiliser IO et quand dois-je utiliser NIO ? Dans cet article, je vais essayer d'expliquer clairement les différences entre Java NIO et IO, leurs scénarios d'utilisation et comment ils affectent la conception de votre code.
Principales différences entre Java NIO et IO
Le tableau suivant résume les principales différences entre Java NIO et IO. Je décrirai les différences dans chaque partie du tableau plus en détail.
IO IO
Orienté flux Orienté tampon
E/S bloquantes E/S non bloquantes
Sélecteurs
Orienté flux et orienté tampon
La première plus grande différence entre Java NIO et IO est que IO est orienté flux et NIO est orienté tampon. Java IO est orienté flux, ce qui signifie qu'un ou plusieurs octets sont lus à la fois dans le flux et que jusqu'à ce que tous les octets soient lus, ils ne sont mis en cache nulle part. De plus, il ne peut pas déplacer les données du flux vers l’avant ou vers l’arrière. Si vous devez déplacer les données lues dans le flux d'avant en arrière, vous devez d'abord les mettre en cache dans un tampon. L'approche orientée tampon de Java NIO est légèrement différente. Les données sont lues dans un tampon qu'elles traitent ultérieurement, en se déplaçant d'avant en arrière dans le tampon selon les besoins. Cela augmente la flexibilité du traitement. Cependant, vous devez également vérifier que le tampon contient toutes les données que vous devez traiter. Assurez-vous également qu'à mesure que davantage de données sont lues dans le tampon, les données non traitées dans le tampon ne sont pas écrasées.
E/S bloquantes et non bloquantes
Divers flux de Java IO sont bloqués. Cela signifie que lorsqu'un thread appelle read() ou write(), le thread est bloqué jusqu'à ce que certaines données soient lues ou que les données soient complètement écrites. Le fil ne peut rien faire d'autre pendant cette période. Le mode non bloquant de Java NIO permet à un thread d'envoyer une demande de lecture de données à partir d'un certain canal, mais il ne peut obtenir que les données actuellement disponibles. Si aucune donnée n'est actuellement disponible, rien ne sera obtenu. Au lieu de maintenir le thread bloqué, celui-ci peut continuer à faire autre chose jusqu'à ce que les données deviennent lisibles. Il en va de même pour les écritures non bloquantes. Un thread demande d'écrire des données sur un canal, mais n'a pas besoin d'attendre qu'elles soient complètement écrites. Le thread peut faire autre chose entre-temps. Les threads utilisent généralement le temps d'inactivité dans les E/S non bloquantes pour effectuer des opérations d'E/S sur d'autres canaux, de sorte qu'un seul thread peut désormais gérer plusieurs canaux d'entrée et de sortie.
Sélecteurs
Les sélecteurs de Java NIO permettent à un seul thread de surveiller plusieurs canaux d'entrée. Vous pouvez enregistrer plusieurs canaux à l'aide d'un sélecteur, puis utiliser un thread séparé pour « sélectionner » les canaux : ces canaux ont déjà une entrée qui peut être traitée. prêt à écrire. Ce mécanisme de sélection permet à un seul thread de gérer facilement plusieurs canaux.
Comment NIO et IO impactent la conception des applications
Que vous choisissiez une boîte à outils IO ou NIO, plusieurs aspects peuvent affecter la conception de votre application :
Appels API aux classes NIO ou IO.
Informatique.
Le nombre de threads utilisés pour traiter les données.
Appel API
Bien sûr, les appels d'API lors de l'utilisation de NIO sont différents de ceux lors de l'utilisation d'IO, mais cela n'est pas inattendu car au lieu de simplement lire octet par octet à partir d'un InputStream, les données doivent d'abord être lues dans un tampon, puis traitées.
Informatique
En utilisant une conception NIO pure par rapport à la conception IO, le traitement des données est également affecté.
Dans la conception IO, nous lisons les données octet par octet à partir d'InputStream ou de Reader. Supposons que vous traitiez un flux de données texte basé sur des lignes, par exemple :
Copiez le code comme suit :
Nom : Anna
Âge : 25 ans
Courriel : [email protected]
Téléphone : 1234567890
Le flux de lignes de texte peut être géré comme ceci :
Copiez le code comme suit :
InputStream input = … ; // récupère le InputStream du socket client
Lecteur BufferedReader = nouveau BufferedReader (nouveau InputStreamReader (entrée));
Chaîne nameLine = reader.readLine();
Chaîne ageLine = reader.readLine();
String emailLine= reader.readLine();
String phoneLine= reader.readLine();
Notez que l'état du traitement est déterminé par la durée d'exécution du programme. En d’autres termes, une fois la méthode reader.readLine() renvoyée, vous savez avec certitude que la ligne de texte a été lue. C’est pourquoi readline() se bloque jusqu’à ce que la ligne entière ait été lue. Vous savez également que cette ligne contient des noms ; de la même manière, lorsque le deuxième appel readline() revient, vous savez que cette ligne contient des âges, etc. Comme vous pouvez le voir, ce gestionnaire ne s'exécute que lorsque de nouvelles données sont lues et sait quelles sont les données à chaque étape. Une fois qu'un thread en cours d'exécution a traité certaines des données qu'il a lues, il ne restaurera pas les données (la plupart du temps). La figure suivante illustre également ce principe :
Lire les données d'un flux bloqué
Bien qu'une implémentation de NIO soit différente, voici un exemple simple :
Copiez le code comme suit :
Tampon ByteBuffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
Notez la deuxième ligne, lisant les octets du canal dans un ByteBuffer. Lorsque cet appel de méthode revient, vous ne savez pas si toutes les données dont vous avez besoin se trouvent dans le tampon. Tout ce que vous savez, c'est que le tampon contient quelques octets, ce qui rend le traitement un peu difficile.
Supposons qu'après le premier appel read(buffer), les données lues dans le tampon ne représentent qu'une demi-ligne, par exemple « Nom : An », pouvez-vous traiter les données ? Évidemment non, vous devez attendre que la totalité de la ligne de données soit lue dans le cache. Avant cela, tout traitement des données n'a aucun sens.
Alors, comment savoir si le tampon contient suffisamment de données à traiter ? Eh bien, vous ne savez pas. Les méthodes découvertes peuvent uniquement afficher les données dans la mémoire tampon. Le résultat est que vous devez vérifier les données du tampon plusieurs fois avant de savoir que toutes les données sont dans le tampon. Non seulement cela est inefficace, mais cela peut également encombrer la solution de programmation. Par exemple:
Copiez le code comme suit :
Tampon ByteBuffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
while(! bufferFull(bytesRead) ) {
bytesRead = inChannel.read(buffer);
}
La méthode bufferFull() doit garder une trace de la quantité de données lues dans le tampon et renvoyer vrai ou faux, selon que le tampon est plein. En d’autres termes, si le tampon est prêt à être traité, il est plein.
La méthode bufferFull() analyse le tampon, mais doit rester dans le même état qu'avant l'appel de la méthode bufferFull(). Dans le cas contraire, les données suivantes lues dans le tampon risquent de ne pas être lues au bon emplacement. C’est impossible, mais c’est encore un autre problème dont il faut être conscient.
Si le tampon est plein, il peut être traité. Si cela ne fonctionne pas et que cela a du sens dans votre cas réel, vous pourrez peut-être en gérer une partie. Mais dans de nombreux cas, ce n’est pas le cas. La figure suivante montre « cycle de données tampon prêt » :
Lire les données d'un canal jusqu'à ce que toutes les données soient lues dans le tampon
Résumer
NIO vous permet de gérer plusieurs canaux (connexions réseau ou fichiers) en utilisant un seul thread (ou quelques-uns), mais le compromis est que l'analyse des données peut être plus complexe que leur lecture à partir d'un flux bloquant.
Si vous devez gérer des milliers de connexions ouvertes simultanément qui n'envoient que de petites quantités de données à chaque fois, comme un serveur de chat, un serveur implémentant NIO peut être un avantage. De même, si vous devez maintenir de nombreuses connexions ouvertes avec d'autres ordinateurs, comme dans un réseau P2P, il peut être avantageux d'utiliser un thread distinct pour gérer toutes vos connexions sortantes. Le schéma de conception de plusieurs connexions dans un seul thread est illustré dans la figure ci-dessous :
Un seul thread gère plusieurs connexions
Si vous disposez d'un petit nombre de connexions utilisant une bande passante très élevée et envoyant de grandes quantités de données à la fois, une implémentation de serveur IO typique pourrait peut-être convenir. La figure suivante illustre une conception typique de serveur IO :
Une conception typique de serveur IO :
Une connexion est gérée par un thread
Canal
Les canaux Java NIO sont similaires aux flux, mais quelque peu différents :
Les données peuvent être lues à partir du canal et les données peuvent être écrites sur le canal. Mais les flux de lecture et d’écriture sont généralement à sens unique.
Les canaux peuvent être lus et écrits de manière asynchrone.
Les données du canal doivent d'abord être lues à partir d'un tampon, ou toujours écrites à partir d'un tampon.
Comme mentionné ci-dessus, les données sont lues du canal vers le tampon et les données sont écrites du tampon vers le canal. Comme indiqué ci-dessous :
Implémentation du canal
Voici les implémentations des canaux les plus importants dans Java NIO :
FileChannel : lire et écrire des données à partir de fichiers.
DatagramChannel : peut lire et écrire des données sur le réseau via UDP.
SocketChannel : peut lire et écrire des données sur le réseau via TCP.
ServerSocketChannel : peut surveiller les connexions TCP entrantes, comme un serveur Web. Un SocketChannel est créé pour chaque nouvelle connexion entrante.
Exemple de canal de base
Voici un exemple d'utilisation de FileChannel pour lire des données dans un Buffer :
Copiez le code comme suit :
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {
System.out.println("Lire " + bytesRead);
buf.flip();
while(buf.hasRemaining()){
System.out.print((char) buf.get());
}
buf.clear();
bytesRead = inChannel.read(buf);
}
aFile.close();
Notez que l'appel à buf.flip() lit d'abord les données dans le Buffer, puis inverse le Buffer, puis lit les données du Buffer. La section suivante entrera plus en détail sur Buffer.
Tampon
Le tampon en Java NIO est utilisé pour interagir avec les canaux NIO. Comme vous le savez, les données sont lues du canal dans le tampon et écrites du tampon dans le canal.
Un tampon est essentiellement un bloc de mémoire dans lequel des données peuvent être écrites et à partir duquel des données peuvent ensuite être lues. Cette mémoire est conditionnée sous forme d'objet NIO Buffer et fournit un ensemble de méthodes pour accéder facilement à cette mémoire.
Utilisation de base de Buffer
L'utilisation de Buffer pour lire et écrire des données suit généralement les quatre étapes suivantes :
Écrire des données dans le tampon
Appeler la méthode flip()
Lire les données de Buffer
Appelez la méthode clear() ou la méthode compact()
Lorsque des données sont écrites dans le tampon, celui-ci enregistre la quantité de données écrites. Une fois que vous souhaitez lire des données, vous devez faire passer le Buffer du mode écriture au mode lecture via la méthode flip(). En mode lecture, toutes les données précédemment écrites dans le tampon peuvent être lues.
Une fois toutes les données lues, le tampon doit être vidé afin de pouvoir y être réécrit. Il existe deux manières de vider le tampon : en appelant la méthode clear() ou compact(). La méthode clear() efface tout le tampon. La méthode compact() effacera uniquement les données lues. Toutes les données non lues sont déplacées au début du tampon et les données nouvellement écrites sont placées après les données non lues dans le tampon.
Voici un exemple d'utilisation de Buffer :
Copiez le code comme suit :
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
//crée un tampon d'une capacité de 48 octets
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf); //lire dans le tampon.
while (bytesRead != -1) {
buf.flip();//rendre le tampon prêt à être lu
while(buf.hasRemaining()){
System.out.print((char) buf.get()); // lit 1 octet à la fois
}
buf.clear(); //prépare le tampon pour l'écriture
bytesRead = inChannel.read(buf);
}
aFile.close();
Capacité, position et limite du tampon
Un tampon est essentiellement un bloc de mémoire dans lequel des données peuvent être écrites et à partir duquel des données peuvent ensuite être lues. Cette mémoire est conditionnée sous forme d'objet NIO Buffer et fournit un ensemble de méthodes pour accéder facilement à cette mémoire.
Afin de comprendre le fonctionnement de Buffer, vous devez connaître ses trois propriétés :
capacité
position
limite
La signification de la position et de la limite dépend du fait que le Buffer soit en mode lecture ou en mode écriture. Quel que soit le mode dans lequel se trouve le Buffer, la signification de la capacité est toujours la même.
Voici une explication de la capacité, de la position et de la limite en mode lecture et écriture, avec des explications détaillées après l'illustration.
capacité
En tant que bloc de mémoire, Buffer a une valeur de taille fixe, également appelée « capacité ». Vous ne pouvez y écrire que des capacités de type octet, long, char et autres. Une fois le tampon plein, il doit être vidé (en lisant ou en effaçant des données) avant de pouvoir continuer l'écriture des données.
position
Lorsque vous écrivez des données dans le tampon, la position représente la position actuelle. La valeur de position initiale est 0. Lorsqu'un octet, une donnée longue, etc. est écrit dans le tampon, la position avance jusqu'à l'unité tampon suivante où les données peuvent être insérées. La position maximale peut être la capacité 1.
Lorsque des données sont lues, elles sont également lues à partir d’un emplacement spécifique. Lors du passage du tampon du mode écriture au mode lecture, la position sera réinitialisée à 0. Lorsque les données sont lues à partir de la position du tampon, la position avance jusqu'à la position lisible suivante.
limite
En mode écriture, la limite du Buffer indique la quantité maximale de données que vous pouvez écrire dans le Buffer. En mode écriture, la limite est égale à la capacité du Buffer.
Lorsque vous passez le tampon en mode lecture, la limite indique la quantité maximale de données que vous pouvez lire. Par conséquent, lors du passage du tampon en mode lecture, la limite sera définie sur la valeur de position en mode écriture. En d'autres termes, vous pouvez lire toutes les données écrites auparavant (la limite est fixée au nombre de données écrites, cette valeur est la position en mode écriture)
Type de tampon
Java NIO possède les types de tampon suivants :
OctetBuffer
MappedByteBuffer
CharBuffer
DoubleBuffer
Tampon flottant
IntBuffer
Tampon Long
Tampon court
Comme vous pouvez le voir, ces types Buffer représentent différents types de données. En d’autres termes, les octets du tampon peuvent être manipulés via les types char, short, int, long, float ou double.
MappedByteBuffer est un peu spécial et sera discuté dans son propre chapitre.
Allocation de tampon
Pour obtenir un objet Buffer, vous devez d'abord l'allouer. Chaque classe Buffer a une méthode d'allocation. Vous trouverez ci-dessous un exemple d'allocation d'un ByteBuffer avec 48 octets de capacité.
Copiez le code comme suit :
ByteBuffer buf = ByteBuffer.allocate(48);
Cela alloue un CharBuffer pouvant stocker 1024 caractères :
Copiez le code comme suit :
CharBuffer buf = CharBuffer.allocate(1024);
Écrire des données dans le tampon
Il existe deux manières d'écrire des données dans Buffer :
Écrivez du canal vers le tampon.
Écrivez dans Buffer via la méthode put() de Buffer.
Exemple d'écriture de Channel vers Buffer
Copiez le code comme suit :
int bytesRead = inChannel.read(buf); //lire dans le tampon.
Exemple d'écriture de Buffer via la méthode put :
Copiez le code comme suit :
buf.put(127);
Il existe de nombreuses versions de la méthode put, vous permettant d'écrire des données dans le Buffer de différentes manières. Par exemple, écrire dans un emplacement spécifié ou écrire un tableau d'octets dans un tampon. Pour plus de détails sur l'implémentation de Buffer, reportez-vous à JavaDoc.
méthode flip()
La méthode flip fait passer le Buffer du mode écriture au mode lecture. L’appel de la méthode flip() remettra la position à 0 et fixera la limite à la valeur de la position précédente.
En d'autres termes, position est maintenant utilisé pour marquer la position de lecture, et limit représente le nombre d'octets, de caractères, etc. qui ont été écrits auparavant - combien d'octets, de caractères, etc. peuvent être lus maintenant.
Lire les données de Buffer
Il existe deux manières de lire les données de Buffer :
Lire les données du tampon vers le canal.
Utilisez la méthode get() pour lire les données du Buffer.
Exemple de lecture de données du Buffer vers le Channel :
Copiez le code comme suit :
//lire du tampon dans le canal.
int octetsWritten = inChannel.write(buf);
Exemple d'utilisation de la méthode get() pour lire des données à partir de Buffer
Copiez le code comme suit :
octet aByte = buf.get();
Il existe de nombreuses versions de la méthode get, vous permettant de lire les données du Buffer de différentes manières. Par exemple, lisez à partir d'une position spécifiée ou lisez les données d'un tampon dans un tableau d'octets. Pour plus de détails sur l'implémentation de Buffer, reportez-vous à JavaDoc.
méthode rembobiner()
Buffer.rewind() remet la position à 0, afin que vous puissiez relire toutes les données du Buffer. La limite reste inchangée et indique toujours combien d'éléments (octet, caractère, etc.) peuvent être lus à partir du Buffer.
Méthodes clear() et compact()
Une fois les données du Buffer lues, le Buffer doit être prêt à y être réécrit. Cela peut être fait via les méthodes clear() ou compact().
Si la méthode clear() est appelée, la position sera remise à 0 et la limite sera fixée à la valeur de capacité. En d’autres termes, le Buffer est vidé. Les données du Buffer ne sont pas effacées, mais ces marques nous indiquent par où commencer à écrire les données dans le Buffer.
S'il y a des données non lues dans le Buffer et que vous appelez la méthode clear(), les données seront "oubliées", ce qui signifie qu'il n'y aura plus de marqueurs pour vous indiquer quelles données ont été lues et lesquelles ne l'ont pas été.
S'il y a encore des données non lues dans le tampon et que les données sont nécessaires plus tard, mais que vous souhaitez d'abord écrire des données, utilisez la méthode compact().
La méthode compact() copie toutes les données non lues au début du Buffer. Définissez ensuite la position juste derrière le dernier élément non lu. L'attribut limit est toujours défini sur capacité comme la méthode clear(). Le tampon est maintenant prêt à écrire des données, mais les données non lues ne seront pas écrasées.
Méthodes mark() et reset()
En appelant la méthode Buffer.mark(), vous pouvez marquer une position spécifique dans le Buffer. Vous pourrez ultérieurement restaurer cette position en appelant la méthode Buffer.reset(). Par exemple:
Copiez le code comme suit :
tampon.mark();
//appelez buffer.get() plusieurs fois, par exemple pendant l'analyse.
buffer.reset();//remettre la position à la marque.
Méthodes equals() et compareTo()
Vous pouvez utiliser les méthodes equals() et compareTo() pour deux Buffers.
est égal()
Lorsque les conditions suivantes sont remplies, cela signifie que les deux Buffers sont égaux :
Avoir le même type (byte, char, int, etc.).
Le nombre d'octets, de caractères, etc. restants dans le tampon est égal.
Tous les octets, caractères, etc. restants dans le tampon sont identiques.
Comme vous pouvez le voir, equals ne compare qu'une partie du Buffer, pas tous les éléments qu'il contient. En fait, il compare uniquement les éléments restants du Buffer.
Méthode compareTo()
La méthode compareTo() compare les éléments restants (octet, caractère, etc.) de deux Buffers Si les conditions suivantes sont remplies, un Buffer est considéré comme « inférieur à » l'autre Buffer :
Le premier élément inégal est plus petit que l'élément correspondant dans l'autre Buffer.
Tous les éléments sont égaux, mais le premier Buffer est épuisé avant l'autre (le premier Buffer contient moins d'éléments que l'autre).
(Annotation : les éléments restants sont les éléments de la position à la limite)
Dispersion/Rassemblement
(Adresse originale de cette partie, auteur : Jakob Jenkov, traducteur : Guo Lei)
Java NIO commence à prendre en charge scatter/gather. Scatter/gather est utilisé pour décrire l'opération de lecture ou d'écriture sur Channel (Note du traducteur : Channel est souvent traduit par canal en chinois).
La lecture dispersée à partir du canal signifie l'écriture des données lues dans plusieurs tampons pendant l'opération de lecture. Par conséquent, le canal « disperse » les données lues depuis le canal dans plusieurs tampons.
Rassembler et écrire sur un canal signifie écrire des données à partir de plusieurs tampons sur le même canal lors d'une opération d'écriture. Par conséquent, le canal « rassemble » les données dans plusieurs tampons et les envoie au canal.
La dispersion/collecte est souvent utilisée dans les situations où les données transmises doivent être traitées séparément. Par exemple, lors de la transmission d'un message composé d'un en-tête de message et d'un corps de message, vous pouvez disperser le corps du message et l'en-tête du message dans différents tampons, de sorte que. vous pouvez facilement traiter les en-têtes et les corps des messages.
Lectures dispersées
Les lectures par diffusion font référence à la lecture de données d'un canal dans plusieurs tampons. Comme décrit dans la figure ci-dessous :
L'exemple de code est le suivant :
Copiez le code comme suit :
En-tête ByteBuffer = ByteBuffer.allocate(128);
Corps de ByteBuffer = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { en-tête, corps } ;
canal.read(bufferArray);
Notez que le tampon est d'abord inséré dans le tableau, puis le tableau est utilisé comme paramètre d'entrée pour channel.read(). La méthode read() écrit les données lues du canal dans le tampon dans l'ordre du tampon dans le tableau. Lorsqu'un tampon est rempli, le canal écrit dans un autre tampon.
Les lectures dispersées doivent remplir le tampon actuel avant de passer au tampon suivant, ce qui signifie également qu'elles ne conviennent pas aux messages dynamiques (Remarque du traducteur : la taille du message n'est pas fixe). En d’autres termes, s’il existe un en-tête et un corps de message, l’en-tête du message doit être entièrement rempli (par exemple, 128 octets) pour que les lectures dispersées fonctionnent correctement.
Rassembler les écritures
La collecte des écritures signifie que les données sont écrites à partir de plusieurs tampons sur le même canal. Comme décrit dans la figure ci-dessous :
L'exemple de code est le suivant :
Copiez le code comme suit :
En-tête ByteBuffer = ByteBuffer.allocate(128);
Corps de ByteBuffer = ByteBuffer.allocate(1024);
//écrit les données dans des tampons
ByteBuffer[] bufferArray = { en-tête, corps } ;
canal.write(bufferArray);
Le tableau buffers est le paramètre d'entrée de la méthode write(). La méthode write() écrira les données dans le canal dans l'ordre des tampons dans le tableau. Notez que seules les données entre la position et la limite seront écrites. Par conséquent, si un tampon a une capacité de 128 octets mais ne contient que 58 octets de données, alors les 58 octets de données seront écrits dans le canal. Par conséquent, contrairement aux lectures dispersées, les écritures rassemblées peuvent mieux gérer les messages dynamiques.
Transfert de données entre canaux
(Adresse originale de cette partie, auteur : Jakob Jenkov, traducteur : Guo Lei, correcteur : Zhou Tai)
Dans Java NIO, si l'un des deux canaux est un FileChannel, vous pouvez alors transférer directement des données d'un canal (Note du traducteur : le canal est souvent traduit par canal en chinois) vers un autre canal.
transfertDe()
La méthode transferFrom() de FileChannel peut transférer des données du canal source vers le FileChannel (Note du traducteur : cette méthode est expliquée dans la documentation du JDK comme le transfert d'octets d'un canal d'octets lisible donné vers le fichier de ce canal. ). Voici un exemple simple :
Copiez le code comme suit :
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
position longue = 0 ;
nombre long = fromChannel.size();
toChannel.transferFrom(position, nombre, fromChannel);
Le paramètre d'entrée position de la méthode indique à partir de la position d'écriture des données dans le fichier cible, et le nombre indique le nombre maximum d'octets transférés. Si le canal source dispose de moins de octets d'espace restant, le nombre d'octets transférés est inférieur au nombre d'octets demandés.
De plus, il convient de noter que lors de l'implémentation de SoketChannel, SocketChannel ne transmettra que les données préparées à ce moment-là (qui peuvent être inférieures au nombre d'octets). Par conséquent, le SocketChannel ne peut pas transférer toutes les données demandées (nombre d’octets) dans le FileChannel.
transfertVers()
La méthode transferTo() transfère les données de FileChannel vers d'autres canaux. Voici un exemple simple :
Copiez le code comme suit :
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
position longue = 0 ;
nombre long = fromChannel.size();
fromChannel.transferTo(position, nombre, toChannel);
Avez-vous trouvé que cet exemple est particulièrement similaire au précédent ? Sauf que l’objet FileChannel appelant la méthode est différent, tout le reste est pareil.
Les problèmes mentionnés ci-dessus à propos de SocketChannel existent également dans la méthode transferTo(). SocketChannel continuera à transmettre des données jusqu'à ce que le tampon cible soit rempli.
Sélecteur
(Lien vers le texte original de cette section, auteur : Jakob Jenkov, traducteur : Langjiv, correcteur : Ding Yi)
Le sélecteur est un composant de Java NIO qui peut détecter un ou plusieurs canaux NIO et savoir si le canal est prêt pour des événements tels que la lecture et l'écriture. De cette manière, un seul thread peut gérer plusieurs canaux et donc plusieurs connexions réseau.
(1)Pourquoi utiliser le sélecteur ?
L'avantage d'utiliser un seul thread pour gérer plusieurs canaux est que moins de threads sont nécessaires pour gérer les canaux. En fait, il est possible de n’utiliser qu’un seul thread pour gérer tous les canaux. Pour le système d'exploitation, le changement de contexte entre les threads est très coûteux et chaque thread occupe certaines ressources système (telles que la mémoire). Par conséquent, moins il y a de fils utilisés, mieux c'est.
Cependant, gardez à l’esprit que les systèmes d’exploitation et les processeurs modernes s’améliorent de plus en plus en matière de multitâche, de sorte que la surcharge du multithreading devient de plus en plus petite au fil du temps. En fait, si un processeur possède plusieurs cœurs, ne pas utiliser le multitâche peut constituer un gaspillage de puissance CPU. Quoi qu’il en soit, la discussion sur cette conception devrait faire l’objet d’un autre article. Ici, il suffit de savoir que vous pouvez gérer plusieurs canaux à l'aide du Sélecteur.
Voici un exemple de diagramme d'un seul thread utilisant un sélecteur pour traiter trois canaux :
(2)Création du sélecteur
Créez un sélecteur en appelant la méthode Selector.open(), comme suit :
Copiez le code comme suit :
Sélecteur sélecteur = Selector.open();
(3) Enregistrez la chaîne avec le sélecteur
Pour utiliser Channel et Selector ensemble, le canal doit être enregistré auprès du sélecteur. Ceci est réalisé via la méthode SelectableChannel.register(), comme suit :
Copiez le code comme suit :
canal.configureBlocking(false);
Clé SelectionKey = canal.register (sélecteur,
Selectionkey.OP_READ);
Lorsqu'il est utilisé avec un sélecteur, le canal doit être en mode non bloquant. Cela signifie que vous ne pouvez pas utiliser FileChannel avec un sélecteur car FileChannel ne peut pas être basculé en mode non bloquant. Les canaux de prise fonctionnent bien.
Notez le deuxième paramètre de la méthode register(). Il s'agit d'une « collection d'intérêts », c'est-à-dire les événements qui vous intéressent lorsque vous écoutez la chaîne via le sélecteur. Il existe quatre types d’événements différents qui peuvent être écoutés :
Connecter
Accepter
Lire
Écrire
Un canal déclenchant un événement signifie que l'événement est prêt. Par conséquent, un canal qui se connecte avec succès à un autre serveur est appelé « prêt à se connecter ». Un canal socket serveur est dit « prêt à recevoir » lorsqu’il est prêt à recevoir des connexions entrantes. Un canal qui a des données à lire est dit « prêt à lire ». Un canal en attente d'écriture de données peut être dit « prêt à l'écriture ».
Ces quatre événements sont représentés par les quatre constantes de SelectionKey :
SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE
Si vous êtes intéressé par plusieurs événements, vous pouvez utiliser l'opérateur OU au niveau du bit pour connecter les constantes, comme suit :
Copiez le code comme suit :
int InterestSet = SelectionKey.OP_READ |
Les collections d'intérêts seront mentionnées ci-dessous.
(4) Clé de sélection
Dans la section précédente, lors de l'enregistrement d'un canal auprès du sélecteur, la méthode register() renvoie un objet SelectionKey. Cet objet contient quelques propriétés qui pourraient vous intéresser :
recouvrement des intérêts
collection prête
Canal
Sélecteur
Objets supplémentaires (facultatif)
Ci-dessous, je décris ces propriétés.
recouvrement des intérêts
Comme décrit dans la section Enregistrer une chaîne auprès d'un sélecteur, la collection d'intérêts est une collection d'événements intéressants que vous sélectionnez. Vous pouvez lire et écrire la collection d'intérêts via SelectionKey, comme ceci :
Copiez le code comme suit :
int InterestSet = selectionKey.interess();
boolean isInterestedInAccept= (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
booléen isInterestedInConnect = InterestSet & SelectionKey.OP_CONNECT ;
booléen isInterestedInRead = InterestSet & SelectionKey.OP_READ ;
booléen isInterestedInWrite = InterestSet & SelectionKey.OP_WRITE ;
On peut voir qu'en utilisant "bit AND" pour faire fonctionner la collection d'intérêts et la constante SelectionKey donnée, vous pouvez déterminer si un certain événement se trouve dans la collection d'intérêts.
collection prête
L'ensemble prêt est l'ensemble des opérations pour lesquelles le canal est prêt. Après une sélection (Sélection), vous accéderez d'abord à l'ensemble prêt. La sélection sera expliquée dans la section suivante. La collection prête est accessible comme ceci :
int readySet = selectionKey.readyOps();
Vous pouvez utiliser la même méthode que la détection de la collecte d'intérêts pour détecter quels événements ou opérations sont prêts dans le canal. Cependant, les quatre méthodes suivantes sont également disponibles, qui renvoient toutes un type booléen :
Copiez le code comme suit :
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
Canal+Sélecteur
Accéder au canal et au sélecteur à partir de SelectionKey est simple. comme suit:
Copiez le code comme suit :
Channelchannel=sélectionKey.channel();
Sélecteur sélecteur = selectionKey.selector();
objets supplémentaires
Un objet ou plusieurs informations peuvent être attachés à la SelectionKey pour identifier facilement un canal donné. Par exemple, vous pouvez attacher un Buffer à utiliser avec un canal ou un objet contenant des données agrégées. Comment l'utiliser :
Copiez le code comme suit :
selectionKey.attach(theObject);
Objet attachéObj = selectionKey.attachment();
Vous pouvez également attacher des objets lors de l'enregistrement du canal auprès du sélecteur à l'aide de la méthode register(). comme:
Copiez le code comme suit :
Clé SelectionKey = canal.register (sélecteur, SelectionKey.OP_READ, theObject);
(5) Sélectionnez la chaîne via le sélecteur
Une fois qu'un ou plusieurs canaux sont enregistrés auprès d'un sélecteur, plusieurs méthodes select() surchargées peuvent être appelées. Ces méthodes renvoient les canaux prêts pour l'événement qui vous intéresse (comme se connecter, accepter, lire ou écrire). En d'autres termes, si vous êtes intéressé par les canaux « prêts à lire », la méthode select() renverra les canaux pour lesquels les événements de lecture sont prêts.
Voici la méthode select() :
int sélectionner()
int select (long délai d'attente)
int selectNow()
select() se bloque jusqu'à ce qu'au moins un canal soit prêt pour l'événement que vous avez enregistré.
select(long timeout) est identique à select(), sauf qu'il bloquera jusqu'à un délai d'attente de quelques millisecondes (paramètre).
selectNow() ne bloque pas et renvoie immédiatement quel que soit le canal prêt (Note du traducteur : cette méthode effectue une opération de sélection non bloquante. Si aucun canal ne devient sélectionnable depuis l'opération de sélection précédente, cette méthode renvoie directement zéro .).
La valeur int renvoyée par la méthode select() indique combien de canaux sont prêts. Autrement dit, combien de canaux sont devenus prêts depuis le dernier appel à la méthode select(). Si la méthode select() est appelée, 1 est renvoyé car un canal devient prêt. Si la méthode select() est appelée à nouveau, si un autre canal est prêt, elle renverra à nouveau 1. Si aucune opération n'est effectuée sur le premier canal prêt, il y a maintenant deux canaux prêts, mais entre chaque appel à la méthode select(), un seul canal est prêt.
Clés sélectionnées()
Une fois que la méthode select() est appelée et que la valeur de retour indique qu'un ou plusieurs canaux sont prêts, les canaux prêts dans le « jeu de clés sélectionné » sont alors accessibles en appelant la méthode selectedKeys() du sélecteur. Comme indiqué ci-dessous :
Copiez le code comme suit :
Définir selectedKeys = selector.selectedKeys();
Lors de l'enregistrement d'un canal comme un sélecteur, la méthode Channel.register() renvoie un objet SelectionKey. Cet objet représente le canal enregistré dans le sélecteur. Ces objets sont accessibles via la méthode selectedKeySet() de SelectionKey.
Les canaux prêts sont accessibles en parcourant cet ensemble de touches sélectionné. comme suit:
Copiez le code comme suit :
Définir selectedKeys = selector.selectedKeys();
Itérateur keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
Clé SelectionKey = keyIterator.next();
if(key.isAcceptable()) {
// une connexion a été acceptée par un ServerSocketChannel.
} sinon if (key.isConnectable()) {
// une connexion a été établie avec un serveur distant.
} sinon if (key.isReadable()) {
// un canal est prêt à être lu
} sinon if (key.isWritable()) {
// un canal est prêt à écrire
}
keyIterator.<tuihighlight><a href="javascript:;" style="display:inline;float:none;position:inherit;cursor:pointer;color:#7962D5;text-decoration:underline;">supprimer</a ></tuihighlight>();
}
Cette boucle parcourt chaque touche de l'ensemble de touches sélectionné et détecte l'événement prêt pour le canal correspondant à chaque touche.
Notez l'appel keyIterator.remove() à la fin de chaque itération. Le sélecteur ne supprime pas lui-même les instances SelectionKey de l’ensemble de clés sélectionné. Doit être supprimé vous-même lorsque la chaîne est traitée. La prochaine fois que le canal sera prêt, le sélecteur le remettra dans le jeu de touches sélectionné.
Le canal renvoyé par la méthode SelectionKey.channel() doit être converti dans le type que vous souhaitez traiter, tel que ServerSocketChannel ou SocketChannel, etc.
(6) réveil ()
Un thread est bloqué après l'appel de la méthode select() Même si aucun canal n'est prêt, il existe un moyen de le renvoyer à partir de la méthode select(). Laissez simplement les autres threads appeler la méthode Selector.wakeup() sur l'objet où le premier thread a appelé la méthode select(). Le thread bloqué sur la méthode select() reviendra immédiatement.
Si un autre thread appelle la méthode wakeup(), mais qu'aucun thread n'est actuellement bloqué sur la méthode select(), le prochain thread qui appelle la méthode select() se "réveillera" immédiatement.
(7)fermer()
L’appel de sa méthode close() après avoir utilisé le sélecteur fermera le sélecteur et invalidera toutes les instances SelectionKey enregistrées dans le sélecteur. Le canal lui-même ne se ferme pas.
(8) Exemple complet
Voici un exemple complet, ouvrez un sélecteur, enregistrez un canal dans le sélecteur (le processus d'initialisation du canal est omis), puis surveillez en permanence si les quatre événements du sélecteur (accepter, connecter, lire, écrire) sont prêts.
Copiez le code comme suit :
Sélecteur sélecteur = Selector.open();
canal.configureBlocking(false);
Clé SelectionKey = canal.register (sélecteur, SelectionKey.OP_READ);
tandis que (vrai) {
int readyChannels = selector.select();
if(readyChannels == 0) continue ;
Définir selectedKeys = selector.selectedKeys();
Itérateur keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
Clé SelectionKey = keyIterator.next();
if(key.isAcceptable()) {
// une connexion a été acceptée par un ServerSocketChannel.
} sinon if (key.isConnectable()) {
// une connexion a été établie avec un serveur distant.
} sinon if (key.isReadable()) {
// un canal est prêt à être lu
} sinon if (key.isWritable()) {
// un canal est prêt à écrire
}
keyIterator.<tuihighlight><a href="javascript:;" style="display:inline;float:none;position:inherit;cursor:pointer;color:#7962D5;text-decoration:underline;">supprimer</a ></tuihighlight>();
}
}
canal de fichier
(Lien vers le texte original de cette section, auteur : Jakob Jenkov, traducteur : Zhou Tai, correcteur : Ding Yi)
FileChannel en Java NIO est un canal connecté à un fichier. Les fichiers peuvent être lus et écrits via des canaux de fichiers.
FileChannel ne peut pas être défini en mode non bloquant, il s'exécute toujours en mode bloquant.
OuvrirFileChannel
Avant d'utiliser FileChannel, il doit être ouvert. Cependant, nous ne pouvons pas ouvrir directement un FileChannel. Nous devons obtenir une instance FileChannel en utilisant un InputStream, OutputStream ou RandomAccessFile. Voici un exemple d'ouverture d'un FileChannel via RandomAccessFile :
Copiez le code comme suit :
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
Lire les données de FileChannel
Appelez l’une des multiples méthodes read() pour lire les données de FileChannel. comme:
Copiez le code comme suit :
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
Tout d’abord, allouez un tampon. Les données lues depuis FileChannel seront lues dans Buffer.
Ensuite, appelez la méthode FileChannel.read(). Cette méthode lit les données de FileChannel dans Buffer. La valeur int renvoyée par la méthode read() indique combien d'octets ont été lus dans le Buffer. S'il renvoie -1, cela signifie que la fin du fichier est atteinte.
Écrire des données dans FileChannel
Utilisez la méthode FileChannel.write() pour écrire des données dans FileChannel. Le paramètre de cette méthode est un Buffer. comme:
Copiez le code comme suit :
String newData = "Nouvelle chaîne à écrire dans le fichier..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
canal.write(buf);
}
Notez que FileChannel.write() est appelé dans une boucle while. Comme il n'y a aucune garantie du nombre d'octets que la méthode write() peut écrire dans le FileChannel à la fois, la méthode write() doit être appelée à plusieurs reprises jusqu'à ce qu'il n'y ait plus d'octets dans le Buffer qui n'aient pas été écrits dans le canal.
FermerFileChannel
Le FileChannel doit être fermé lorsque vous avez terminé. comme:
Copiez le code comme suit :
canal.close();
Méthode de positionnement FileChannel
Parfois, il peut être nécessaire de lire/écrire des données à un emplacement spécifique du FileChannel. Vous pouvez obtenir la position actuelle de FileChannel en appelant la méthode position().
Vous pouvez également définir la position actuelle de FileChannel en appelant la méthode position(long pos).
Voici deux exemples :
Copiez le code comme suit :
pos long = canal.position();
canal.position(pos +123);
Si vous définissez la position après la fin du fichier et essayez ensuite de lire les données du canal de fichier, la méthode de lecture renverra -1 - l'indicateur de fin de fichier.
Si vous définissez la position après la fin du fichier et que vous écrivez ensuite des données sur le canal, le fichier sera étendu à la position actuelle et les données seront écrites. Cela peut entraîner des « trous de fichiers », des écarts entre les données écrites dans les fichiers physiques du disque.
Méthode de taille FileChannel
La méthode size() d'une instance FileChannel renverra la taille du fichier associé à l'instance. comme:
Copiez le code comme suit :
long fileSize = canal.size();
Méthode tronquée de FileChannel
Vous pouvez utiliser la méthode FileChannel.truncate() pour intercepter un fichier. Lors de l'interception d'un fichier, la partie après la longueur spécifiée du fichier sera supprimée. comme:
Copiez le code comme suit :
canal.truncate(1024);
Cet exemple intercepte les 1 024 premiers octets du fichier.
Forcer la méthode de FileChannel
La méthode FileChannel.force() force les données du canal qui n'ont pas encore été écrites sur le disque vers le disque. Pour des raisons de performances, le système d'exploitation met les données en cache dans la mémoire. Il n'y a donc aucune garantie que les données écrites sur FileChannel seront immédiatement écrites sur le disque. Pour garantir cela, la méthode force() doit être appelée.
La méthode force() possède un paramètre booléen qui indique s'il faut écrire simultanément les métadonnées du fichier (informations d'autorisation, etc.) sur le disque.
L'exemple suivant force les données de fichier et les métadonnées sur le disque :
Copiez le code comme suit :
canal.force(true);
Canal de prise
(Lien vers le texte original de cette section, auteur : Jakob Jenkov, traducteur : Zheng Yuting, correcteur : Ding Yi)
SocketChannel en Java NIO est un canal connecté à un socket réseau TCP. SocketChannel peut être créé des deux manières suivantes :
Ouvrez un SocketChannel et connectez-vous à un serveur sur Internet.
Lorsqu'une nouvelle connexion arrive à ServerSocketChannel, un SocketChannel est créé.
Ouvrir SocketChannel
Voici comment ouvrir SocketChannel :
Copiez le code comme suit :
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
Fermer SocketChannel
Lorsque vous avez terminé avec SocketChannel, appelez SocketChannel.close() pour fermer SocketChannel :
Copiez le code comme suit :
socketChannel.close();
Lire les données de SocketChannel
Pour lire les données d'un SocketChannel, appelez l'une des méthodes read(). Voici des exemples :
Copiez le code comme suit :
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);
Tout d’abord, allouez un tampon. Les données lues depuis SocketChannel seront placées dans ce Buffer.
Ensuite, appelez SocketChannel.read(). Cette méthode lit les données de SocketChannel dans Buffer. La valeur int renvoyée par la méthode read() indique combien d'octets ont été lus dans le Buffer. Si -1 est renvoyé, cela signifie que la fin du flux a été lue (la connexion a été fermée).
Écrire sur SocketChannel
L'écriture de données sur SocketChannel utilise la méthode SocketChannel.write(), qui prend un Buffer comme paramètre. Les exemples sont les suivants :
Copiez le code comme suit :
String newData = "Nouvelle chaîne à écrire dans le fichier..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
canal.write(buf);
}
Notez que la méthode SocketChannel.write() est appelée dans une boucle while. La méthode Write() ne peut pas garantir le nombre d'octets pouvant être écrits dans SocketChannel. Nous appelons donc write() à plusieurs reprises jusqu'à ce que le Buffer n'ait plus d'octets à écrire.
mode non bloquant
Vous pouvez définir SocketChannel en mode non bloquant. Après le réglage, vous pouvez appeler connect(), read() et write() en mode asynchrone.
connecter()
Si SocketChannel est en mode non bloquant et que connect() est appelé à ce moment-là, la méthode peut revenir avant que la connexion ne soit établie. Pour déterminer si la connexion est établie, vous pouvez appeler la méthode finishConnect(). Comme ça:
Copiez le code comme suit :
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
while(! socketChannel.finishConnect() ){
//attends, ou fais autre chose...
}
écrire()
En mode non bloquant, la méthode write() peut revenir avant d'écrire quoi que ce soit. Donc write() doit être appelé dans la boucle. Il y a eu des exemples auparavant, je n’entrerai donc pas dans les détails ici.
lire()
En mode non bloquant, la méthode read() peut revenir avant que les données n'aient été lues. Vous devez donc faire attention à sa valeur de retour int, qui vous indiquera combien d'octets ont été lus.
Mode et sélecteurs non bloquants
Le mode non bloquant fonctionne mieux avec les sélecteurs. En enregistrant un ou plusieurs SocketChannels avec le sélecteur, vous pouvez demander au sélecteur quel canal est prêt pour la lecture, l'écriture, etc. La combinaison de Selector et SocketChannel sera discutée en détail plus tard.
Canal ServerSocket
(Lien vers le texte original de cette section, auteur : Jakob Jenkov, traducteur : Zheng Yuting, correcteur : Ding Yi)
ServerSocketChannel dans Java NIO est un canal qui peut écouter les nouvelles connexions TCP entrantes, tout comme ServerSocket dans IO standard. La classe ServerSocketChannel se trouve dans le package java.nio.channels.
Voici un exemple :
Copiez le code comme suit :
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
tandis que(vrai){
SocketChannel socketChannel =
serverSocketChannel.accept();
//faire quelque chose avec socketChannel...
}
Ouvrir ServerSocketChannel
Ouvrez le ServerSocketChannel en appelant la méthode ServerSocketChannel.open(). Par exemple :
Copiez le code comme suit :
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
Fermer ServerSocketChannel
Fermez ServerSocketChannel en appelant la méthode ServerSocketChannel.close(). Par exemple :
Copiez le code comme suit :
serverSocketChannel.close();
Écoutez les nouvelles connexions entrantes
Écoutez les nouvelles connexions entrantes via la méthode ServerSocketChannel.accept(). Lorsque la méthode accept() est renvoyée, elle renvoie un SocketChannel contenant la nouvelle connexion entrante. Par conséquent, la méthode accept() se bloquera jusqu’à l’arrivée d’une nouvelle connexion.
Habituellement, au lieu d'écouter simplement une connexion, la méthode accept() est appelée dans la boucle while. Comme dans l'exemple suivant :
Copiez le code comme suit :
tandis que(vrai){
SocketChannel socketChannel =
serverSocketChannel.accept();
//faire quelque chose avec socketChannel...
}
Bien entendu, vous pouvez également utiliser d’autres critères de sortie que true dans la boucle while.
mode non bloquant
ServerSocketChannel peut être défini en mode non bloquant. En mode non bloquant, la méthode accept() retournera immédiatement s'il n'y a pas de nouvelle connexion entrante, la valeur de retour sera nulle. Par conséquent, vous devez vérifier si le SocketChannel renvoyé est nul. comme:
Copiez le code comme suit :
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);
tandis que(vrai){
SocketChannel socketChannel =
serverSocketChannel.accept();
si(socketChannel != null){
//faire quelque chose avec socketChannel...
}
}
Canal de datagramme
(Lien vers le texte original de cette section, auteur : Jakob Jenkov, traducteur : Zheng Yuting, correcteur : Ding Yi)
DatagramChannel en Java NIO est un canal qui peut envoyer et recevoir des paquets UDP. UDP étant un protocole réseau sans connexion, il ne peut pas être lu et écrit comme les autres canaux. Il envoie et reçoit des paquets de données.
OpenDatagramChannel
Voici comment DatagramChannel est ouvert :
Copiez le code comme suit :
Canal DatagramChannel = DatagramChannel.open();
canal.socket().bind(new InetSocketAddress(9999));
Le DatagramChannel ouvert par cet exemple peut recevoir des paquets sur le port UDP 9999.
recevoir des données
Recevez des données de DatagramChannel via la méthode recevoir(), telle que :
Copiez le code comme suit :
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
canal.receive(buf);
La méthode recevoir() copiera le contenu du paquet de données reçu dans le tampon spécifié. Si le tampon ne peut pas accueillir les données reçues, les données excédentaires seront supprimées.
Envoyer des données
Envoyez des données depuis DatagramChannel via la méthode send(), telle que :
Copiez le code comme suit :
String newData = "Nouvelle chaîne à écrire dans le fichier..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
int bytesSent = canal.send(buf, new InetSocketAddress("jenkov.com", 80));
Cet exemple envoie une chaîne de caractères au port UDP 80 du serveur "jenkov.com". Puisque le serveur ne surveille pas ce port, rien ne se passera. Il ne vous indiquera pas non plus si le paquet sortant a été reçu, car UDP n'a aucune garantie en termes de livraison des données.
Connectez-vous à une adresse spécifique
Un DatagramChannel peut être « connecté » à une adresse spécifique du réseau. Puisque UDP est sans connexion, la connexion à une adresse spécifique ne crée pas de véritable connexion comme un canal TCP. Au lieu de cela, le DatagramChannel est verrouillé afin qu'il ne puisse envoyer et recevoir des données qu'à partir d'une adresse spécifique.
Voici un exemple :
Copiez le code comme suit :
canal.connect(new InetSocketAddress("jenkov.com", 80));
Une fois connecté, vous pouvez également utiliser les méthodes read() et write() comme vous le feriez avec un canal traditionnel. Il n'y a tout simplement aucune garantie concernant le transfert de données. Voici quelques exemples :
Copiez le code comme suit :
int bytesRead = canal.read(buf);
int bytesWritten = canal.write(mais);
Tuyau
(Lien vers le texte original de cette section, auteur : Jakob Jenkov, traducteur : Huang Zhong, correcteur : Ding Yi)
Un canal Java NIO est une connexion de données unidirectionnelle entre 2 threads. Pipe a un canal source et un canal récepteur. Les données seront écrites sur le canal récepteur et lues à partir du canal source.
Voici une illustration du principe Pipe :
Créer un pipeline
Ouvrez le canal via la méthode Pipe.open(). Par exemple:
Copiez le code comme suit :
Pipe pipe = Pipe.open();
Écrire des données dans le tube
Pour écrire des données dans le canal, vous devez accéder au canal récepteur. Comme ça:
Copiez le code comme suit :
Pipe.SinkChannel SinkChannel = pipe.sink();
Écrivez des données dans SinkChannel en appelant la méthode write() de SinkChannel, comme ceci :
Copiez le code comme suit :
String newData = "Nouvelle chaîne à écrire dans le fichier..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
<b>sinkChannel.write(buf);</b>
}
[code]
Lire les données du tuyau
Pour lire les données d'un canal, vous devez accéder au canal source, comme ceci :
[code]
Pipe.SourceChannel sourceChannel = pipe.source();
Appelez la méthode read() du canal source pour lire les données, comme ceci :
Copiez le code comme suit :
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
La valeur int renvoyée par la méthode read() nous indiquera combien d'octets ont été lus dans le tampon.