Connaissances de base
Synchrone, asynchrone, blocage, non bloquant
Tout d'abord, ces concepts sont très faciles à confondre, mais ils sont impliqués dans NIO, alors résumons-le.
Synchronisation: Lorsque l'appel API revient, l'appelant sait comment l'opération en résulte (combien d'octets sont réellement lus / écrits).
Asynchrone: par rapport à la synchronisation, l'appelant ne connaît pas le résultat de l'opération lorsque l'appel API renvoie, et le rappel informera le résultat plus tard.
Blocage: Lorsqu'il n'y a pas de données à lire ou que toutes les données ne peuvent pas être écrites, suspendre le thread actuel en attente.
Non-blocking: lors de la lecture, vous pouvez lire autant que les données que vous pouvez lire, puis revenir. Lors de l'écriture, vous pouvez écrire autant que les données que vous pouvez écrire, puis revenir.
Pour les opérations d'E / S, selon les documents du site officiel d'Oracle, la norme de synchronisation et de division asynchrone est "si l'appelant doit attendre que l'opération d'E / S se termine". Cette "attente de l'opération d'E / S ne signifie pas que les données doivent être lues ou que toutes les données doivent être écrites, mais plutôt si l'appelant doit attendre lorsque l'opération d'E / S est réellement effectuée, comme le moment où les données sont transmises entre le tampon de pile de protocole TCP / IP et le tampon JVM.
Par conséquent, nos méthodes Read () et Write () couramment utilisées sont des E / S synchrones. Les E / S synchrones sont divisées en deux modes: blocage et non bloquant. S'il s'agit d'un mode non bloquant, il sera retourné directement lorsqu'il détecte qu'il n'y a pas de données à lire et que l'opération d'E / S n'est pas vraiment effectuée.
En résumé, en Java, il n'y a en fait que trois mécanismes: les E / S de blocage synchrone, les E / S non bloquantes synchrones et les E / S asynchrone. Ce dont nous parlons ci-dessous, c'est les deux premiers. JDK1.7 a commencé à présenter des E / S asynchrones, qui s'appelle Nio.2.
IO traditionnel
Nous savons que l'émergence d'une nouvelle technologie s'accompagne toujours d'améliorations et d'améliorations, tout comme l'émergence de Javanio.
Les E / S traditionnelles bloquent les E / S, et le principal problème est le gaspillage des ressources système. Par exemple, afin de lire les données d'une connexion TCP, nous appelons la méthode Read () de InputStream, qui entraînera la suspension du thread actuel et ne sera pas éveillée avant l'arrivée des données. Le thread occupe les ressources de mémoire (pile de threads de stockage) pendant la période d'arrivée des données, mais ne fait rien. C'est ce que va le dicton, occupant la fosse et non le caca. Afin de lire les données d'autres connexions, nous devons démarrer un autre thread. Cela peut être bien lorsqu'il n'y a pas beaucoup de connexions simultanées, mais lorsque le nombre de connexions atteint une certaine échelle, les ressources de mémoire seront consommées par un grand nombre de threads. D'un autre côté, la commutation de thread nécessite de modifier l'état du processeur, telles que les valeurs des compteurs et des registres de programme, donc le changement entre un grand nombre de threads est également un gaspillage de ressources.
Avec le développement de la technologie, les systèmes d'exploitation modernes fournissent de nouveaux mécanismes d'E / S qui peuvent éviter ce gaspillage de ressources. Sur la base de cela, Javanio est né et la caractéristique représentative de NIO est des E / S non bloquantes. Immédiatement après, nous avons constaté que l'utilisation des E / S non bloquantes ne peut pas résoudre le problème, car en mode non bloquant, la méthode read () reviendra immédiatement lorsque les données ne seront pas lues. Nous ne savons pas quand les données arriveront, nous ne pouvons donc continuer à appeler la méthode Read () pour réessayer. C'est évidemment un gaspillage de ressources CPU. À partir de ce qui suit, nous pouvons savoir que le composant sélecteur est né pour résoudre ce problème.
Composants de noyau Javanio
1.Channel
concept
Toutes les opérations d'E / S dans Javanio sont basées sur des objets de canal, tout comme les opérations de flux sont basées sur des objets de flux, il est donc nécessaire de comprendre d'abord ce que le canal est. Le contenu suivant est extrait de la documentation de JDK1.8
Achannel représente la connexion de l'annexe à l'annexité de telasahardwaredevice, afile, annetworksocket, un composant oroprogram qui peut être effectuée sur une seule I / Ooperations distinctes, un échantillonnage ou une écriture.
À partir du contenu ci-dessus, nous pouvons voir qu'un canal représente une connexion à une certaine entité, qui peut être un fichier, une prise de réseau, etc. En d'autres termes, le canal est un pont fourni par Javanio pour que nos programmes interagissent avec les services d'E / S sous-jacents du système d'exploitation.
Les canaux sont une description très basique et abstraite, interagissent avec différents services d'E / S, effectuent différentes opérations d'E / S et implémentent différentes implémentations. Par conséquent, les spécifiques incluent FileChannel, Socketchannel, etc.
Le canal est similaire au flux lorsqu'il est utilisé. Il peut lire des données dans un tampon ou écrire des données dans le tampon sur le canal.
Bien sûr, il existe également des différences, qui se reflètent principalement dans les deux points suivants:
Un canal peut être lu et écrit, tandis qu'un flux est unidirectionnel (donc divisé en entrée et en sortie)
Le canal a un mode d'E / S non bloquant
accomplir
Les implémentations de canaux les plus couramment utilisées dans Javanio sont les suivantes, et on peut voir qu'elles correspondent aux classes d'opération d'E / S traditionnelles une par une.
FileChannel: Lire et écrire des fichiers
DatagramChannel: Communication du réseau de protocole UDP
Socketchannel: communication de réseau de protocole TCP
SERVERSOCHETHETCHANNEL: Écoutez les connexions TCP
2.Buffer
Le tampon utilisé dans NIO n'est pas un tableau d'octets simple, mais une classe de tampon encapsulée. Grâce à l'API qu'il fournit, nous pouvons manipuler de manière flexible les données. Regardons de plus près.
Correspondant aux types de base Java, Nio fournit une variété de types de tampons, tels que ByteBuffer, Charbuffer, Intbuffer, etc. La différence est que la longueur d'unité du tampon est différente lors de la lecture et de l'écriture (lecture et écriture en unités des variables de type correspondantes).
Il existe 3 variables très importantes dans le tampon. Ils sont la clé pour comprendre le mécanisme de travail du tampon, à savoir
capacité (capacité totale)
position (la position actuelle du pointeur)
limite (lire / écrire la position limite)
La méthode de travail du tampon est très similaire aux tableaux de caractères en C. En analogie, la capacité est la longueur totale du tableau, la position est la variable d'indice pour nous de lire / écrire des caractères et la limite est la position du caractère final. La situation des 3 variables au début du tampon est la suivante
Pendant le processus de lecture / rédaction du tampon, la position reviendra en arrière et la limite est la limite du mouvement de position. Il n'est pas difficile d'imaginer que lors de l'écriture dans un tampon, la limite doit être définie sur la taille de la capacité, et lors de la lecture dans un tampon, la limite doit être définie sur la position finale réelle des données. (Remarque: l'écriture de données de tampon sur la chaîne est une opération de lecture de tampon, et la lecture des données du canal vers le tampon est une opération d'écriture de tampon)
Avant de lire / écrire des opérations sur un tampon, nous pouvons appeler certaines méthodes auxiliaires fournies par la classe de tampon pour définir correctement les valeurs de position et de limite, principalement comme suit
flip (): définissez la limite à la valeur de la position, puis définissez la position sur 0. Appels avant de lire le tampon.
Rewind (): Il suffit de définir la position 0. Il est généralement appelé avant de relire les données du tampon, par exemple, il sera utilisé lors de la lecture des données du même tampon et de l'écriture sur plusieurs canaux.
Clear (): Retour à l'état initial, c'est-à-dire que la limite est égale à la capacité, la position définie sur 0. Appelez le tampon avant d'écrire.
compact (): déplacer les données non lues (données entre la position et la limite) au début du tampon et définir la position à la position suivante à la fin de ces données. En fait, il équivaut à réécrire à nouveau un tel morceau de données dans le tampon.
Ensuite, regardez un exemple, utilisez FileChannel pour lire et écrire des fichiers texte, et utilisez cet exemple pour vérifier les caractéristiques lisibles et écrit de la chaîne et l'utilisation de base du tampon (notez que FileChannel ne peut pas être défini sur le mode non bloquant).
FileChannel Channel = new RandomAccessFile ("test.txt", "rw"). GetChannel (); channel.position (canal.size ()); // déplace le pointeur de fichier vers la fin (annex World! / N ".getBytes (StandardCharsets.Utf_8)); // Buffer -> ChannelByteBuffer.flip (); While (ByteBuffer.HasreMaining ()) {Channel.Write (ByteBuffer);} Channel.position (0); // Déplace le pointeur de fichier. CharSetDecoder Decoder = StandardCharsets.Utf_8.newDecoder (); // Lisez toutes les données ByteBuffer.Clear (); while (channel.read (bytebuffer)! = -1 || bytebuffer.position ()> 0) {bytebuffer.flip (); // decode charbuffer.clear (); decoder.decode (bytebuffer, charbuffer, false); System.out.print (Charbuffer.flip (). ToString ()); byteBuffer.compact (); // il peut y avoir des données restant} canal.close ();Dans cet exemple, deux tampons sont utilisés, où ByteBuffer est le tampon de données pour la lecture et l'écriture des canaux, et Charbuffer est utilisé pour stocker des caractères décodés. L'utilisation de Clear () et Flip () est comme mentionné ci-dessus. Il convient de noter que la dernière méthode compacte () est, même si la taille du charbuffer est complètement suffisante pour s'adapter aux data bytebuffer décodées, ce compact () est également essentiel. En effet, le codage UTF-8 des caractères chinois couramment utilisés représente 3 octets, il y a donc une forte probabilité que cela se produise dans la troncature moyenne. Veuillez consulter la figure ci-dessous:
Lorsque le décodeur lit 0xe4 à la fin du tampon, il ne peut pas être mappé à un Unicode. Le troisième paramètre de la méthode decode (), FALSE, est utilisé pour faire en sorte que le décodeur traite les octets non apparables et les données suivantes sous forme de données supplémentaires. Par conséquent, la méthode decode () s'arrêtera ici et la position retombera à la position 0xe4. De cette façon, le premier octet codé par le mot "support" est laissé dans le tampon, et il doit être compacté à l'avant et épissé avec les données de séquence correctes et suivantes. En ce qui concerne le codage des caractères, vous pouvez vous référer à " Explication des concepts ANSI, Unicode, BMP, UTF et d'autres concepts de codage "
BTW, le charsetdecoder dans l'exemple est également une nouvelle fonctionnalité de Javanio, donc vous auriez dû découvrir un peu. Les opérations NIO sont orientées vers le tampon (les E / S traditionnelles sont orientées vers le flux).
À ce stade, nous avons appris l'utilisation de base du canal et du tampon. Ensuite, nous parlerons des composants importants de la permission d'un fil à gérer plusieurs canaux.
3.Sélecteur
Qu'est-ce que le sélecteur
Le sélecteur est un composant spécial utilisé pour collecter l'état (ou les événements) de chaque canal. Nous enregistrons d'abord le canal vers le sélecteur et définissons l'événement dont nous nous soucions, puis nous pouvons attendre tranquillement que l'événement se produise en appelant la méthode Select ().
La chaîne a les 4 événements suivants à écouter:
Accepter: il existe une connexion acceptable
Connectez: connectez avec succès
Lire: il y a des données à lire
Écrire: vous pouvez écrire des données
Pourquoi utiliser le sélecteur
Comme mentionné ci-dessus, si vous utilisez des E / S de blocage, vous devez multiplier (un gaspillage de mémoire), et si vous utilisez des E / S non bloquantes, vous devez réessayer constamment (une consommation de CPU). L'émergence du sélecteur résout ce problème embarrassant. En mode non bloquant, via le sélecteur, nos threads ne fonctionnent que pour les canaux prêts, et il n'est pas nécessaire d'essayer aveuglément. Par exemple, lorsqu'aucune donnée n'est atteinte dans tous les canaux, aucun événement de lecture ne se produit et notre fil sera suspendu à la méthode SELECT (), abandonnant ainsi les ressources CPU.
Comment utiliser
Comme indiqué ci-dessous, créez un sélecteur et enregistrez un canal.
Remarque: Pour enregistrer le canal vers le sélecteur, vous devez d'abord définir le canal en mode non bloquant, sinon une exception sera lancée.
Selector Selector = Selector.Open (); Channel.ConfigureBlocking (FALSE); SelectionKey Key = Channel.Register (Selector, SelectionKey.op_Read);
Le deuxième paramètre de la méthode Register () est appelé "Set d'intérêt", qui est l'ensemble d'événements qui vous préoccupe. Si vous vous souciez de plusieurs événements, séparez-les par un "bital ou opérateur", par exemple
SelectionKey.op_read | Selectionkey.op_write
Cette méthode d'écriture n'est pas familière. Il est joué dans des langages de programmation qui prennent en charge les opérations de bit. L'utilisation d'une variable entière peut identifier plusieurs états. Comment est-ce fait? C'est en fait très simple. Par exemple, d'abord certaines constantes prédéfinies, et leurs valeurs (binaires) sont les suivantes
On peut constater que les bits avec leur valeur de 1 sont tous échelonnés, de sorte que les valeurs obtenues après avoir effectué le sens bit ou les calculs n'ont pas d'ambiguïté, et ils peuvent être déduits inversement de quels variables sont calculées. Comment juger, oui, c'est le "Bits et" l'opération. Par exemple, il existe maintenant une valeur de variable de définition d'état de 0011. Nous devons seulement déterminer si la valeur de "0011 & op_read" est 1 ou 0 pour déterminer si l'ensemble contient l'état OP_READ.
Ensuite, notez que la méthode Register () renvoie un objet SelectionKey, qui contient les informations pour cet enregistrement, et nous pouvons également modifier les informations d'enregistrement via l'informatique. À partir de l'exemple complet ci-dessous, nous pouvons voir qu'après Select (), nous proposons également les canaux avec l'état en obtenant une collection de SELECTIONKEYS.
Un exemple complet
Les concepts et les choses théoriques ont été expliqués (en fait, après les avoir écrits ici, j'ai trouvé que je n'avais pas beaucoup écrit, ce qui est tellement embarrassant (⊙ˍ⊙)). Jetons un coup d'œil à un exemple complet.
Cet exemple utilise Javanio pour implémenter un serveur unique. La fonction est très simple. Il écoute la connexion client. Lorsque la connexion est établie, il lit le message du client et répond à un message au client.
Il convient de noter que j'utilise le caractère '/ 0' (un octet avec une valeur de 0) pour identifier la fin du message.
Serveur fileté unique
classe publique NiOServer {public static void main (String [] args) lève ioException {// Créer un SelectorSelector Selector = Selector.Open (); // Initialisez le canal d'écoute de la connexion TCP ServersocketChannel ListEnchannel = serversOCHANTECHELL.Open (); listEnchannel.bind (new InetsocketAddress (9999)); listEnchannel.configureBlocking (false); // Inscrivez-vous sur sélecteur (écoutez son événement d'acceptation) listEnchannel.Register (Selector, SelectionKey.op_Accept); // Créer un tampon ByteBuffer Buffer = ByteBuffer.Allocate (100); tandis que (true) {Selector.Select (); // bloque jusqu'à ce qu'un événement soit écouté se produit iterator <lelectionkey> keyiter = selector.selectedkeys (). iterator (); // accédez à l'événement de la chaîne sélectionnée via un itérateur à son tour (keyiter.hasnext ()) {SelectionKey key = keyiter.next () Socketchannel Channel = ((Serversocketchannel) key.channel ()). Accepte (); channel.configureBlocking (false); channel.register (selector, selectionkey.op_read); System.out.println ("connexion avec [" + channel.getReMoteDdress () + "]!") tamper.clear (); // lu à la fin du flux, indiquant que la connexion TCP a été déconnectée, // Par conséquent, il est nécessaire de fermer le canal ou d'annuler l'événement de lecture // sinon, il bouclera infiniment if (((socketchannel) key.channel ()). Read (buffer) == -1) {key.Channel (). buffer.flip (); while (buffer.hasreminging ()) {byte b = buffer.get (); if (b == 0) {// /0System.out.println () à la fin du message client; // Response Client Buffer.Clear (); Buffer.put ("Hello, client! / 0" .getBytes ()); Buffer.flip (); {((Socketchannel) key.channel ()). Write (buffer);}} else {System.out.print ((char) b);}}} // Pour les événements qui ont été traités, vous devez supprimer manuellement keyiter.remove ();}}}}Client
Ce client est purement utilisé pour les tests. Afin de le rendre moins difficile, il utilise des méthodes d'écriture traditionnelles et le code est très court.
Si vous devez être plus rigoureux dans les tests, vous devez exécuter un grand nombre de clients simultanément pour compter le temps de réponse du serveur et n'envoyez pas de données immédiatement après l'établissement de la connexion, afin de donner un jeu complet aux avantages des E / S non bloquantes sur le serveur.
Client de classe publique {public static void main (String [] args) lève une exception {socket socket = new socket ("localhost", 9999); inputStream is = socket.getInputStream (); outputStream os = socket.getOutputStream (); // Envoyer des données au serveur First OS.Write ("Hello, Server! / 0" .getBytes (); while ((b = is.read ())! = 0) {System.out.print ((char) b);} System.out.println (); socket.close ();}}Résumer
Ce qui précède concerne une compréhension rapide des composants Nio Core en Java. J'espère que ce sera utile à tout le monde. Les amis intéressés peuvent continuer à se référer à d'autres contenus pertinents de ce site Web. S'il y a des lacunes, veuillez laisser un message pour le signaler. Merci vos amis pour votre soutien pour ce site!