Hintergrundwissen
Synchron, asynchron, blockiert, nicht blockiert
Erstens sind diese Konzepte sehr leicht zu verwirren, aber sie sind in NIO beteiligt. Lassen Sie uns es also zusammenfassen.
Synchronisation: Wenn der API -Aufruf zurückkehrt, weiß der Anrufer, wie der Vorgang entsteht (wie viele Bytes tatsächlich gelesen/schreiben).
Asynchron: Im Vergleich zur Synchronisation kennt der Anrufer das Ergebnis der Operation nicht, wenn der API -Aufruf zurückkehrt, und der Rückruf wird das Ergebnis später benachrichtigt.
Blockierung: Wenn keine Daten gelesen werden oder alle Daten geschrieben werden können, werden Sie den aktuellen Thread warten.
Nicht-Blocking: Beim Lesen können Sie so viel wie die Daten lesen, die Sie lesen und dann zurückkehren können. Beim Schreiben können Sie so viel wie die Daten schreiben, die Sie schreiben und dann zurückkehren können.
Für I/A -Operationen lautet die Synchronisation und der asynchrone Divisionsstandard gemäß den Dokumenten der offiziellen Website von Oracle, "ob der Anrufer warten muss, bis der E/A -Betrieb abgeschlossen ist". Dieses "Warten auf die E/A -Operation" bedeutet nicht, dass Daten gelesen oder alle Daten geschrieben werden müssen, sondern ob der Anrufer warten muss, wenn der E/A -Betrieb tatsächlich durchgeführt wird, z.
Daher sind unsere häufig verwendeten Methoden read () und write () synchron E/O. Die synchrone E/A ist in zwei Modi unterteilt: Blockierung und Nichtblockierung. Wenn es sich um einen nicht blockierenden Modus handelt, wird er direkt zurückgegeben, wenn festgestellt wird, dass keine Daten gelesen werden und die E/A-Operation nicht wirklich ausgeführt wird.
Zusammenfassend gibt es in Java tatsächlich nur drei Mechanismen: Synchronous Blocking I/O, synchrones nicht blockierendes E/A und asynchrones I/O. Was wir unten sprechen, sind die ersten beiden. JDK1.7 hat begonnen, asynchrone E/O einzuführen, die nio.2 genannt wird.
Traditionelles IO
Wir wissen, dass die Entstehung einer neuen Technologie immer von Verbesserungen und Verbesserungen begleitet wird, ebenso wie die Entstehung von Javanio.
Das traditionelle E/A blockiert E/A, und das Hauptproblem ist die Verschwendung von Systemressourcen. Um beispielsweise die Daten einer TCP -Verbindung zu lesen, rufen wir die Read () -Methode von InputStream auf, wodurch der aktuelle Thread suspendiert wird und erst erweckt wird, wenn die Daten eintrifft. Der Thread nimmt während der Zeit der Dateneinführung Speicherressourcen (Speicher -Thread -Stapel) ein, tut jedoch nichts. Das ist das, was das Sprichwort besetzt, die Grube und nicht die Kacke. Um die Daten anderer Verbindungen zu lesen, müssen wir einen anderen Thread starten. Dies mag in Ordnung sein, wenn nicht viele gleichzeitige Verbindungen vorhanden sind, aber wenn die Anzahl der Verbindungen eine bestimmte Skala erreicht, werden Speicherressourcen von einer großen Anzahl von Threads verbraucht. Andererseits erfordert das Umschalten von Threads die Änderung des Status des Prozessors, wie z. B. die Werte von Programmzählern und Registern, so
Mit der Entwicklung der Technologie bieten moderne Betriebssysteme neue E/A -Mechanismen, die diese Verschwendung von Ressourcen vermeiden können. Basierend darauf wurde Javanio geboren, und das repräsentative Merkmal von NIO ist nicht blockierende I/O. Sofort danach stellten wir fest, dass die einfach nicht blockierende I/A das Problem nicht lösen kann, da die Methode read () im Nicht-Blocking-Modus sofort zurückgibt, wenn die Daten nicht gelesen werden. Wir wissen nicht, wann die Daten eintreffen, sodass wir nur die Read () -Methode anrufen können, um es erneut zu versuchen. Dies ist offensichtlich eine Verschwendung von CPU -Ressourcen. Aus den folgenden können wir wissen, dass die Selektorkomponente geboren wurde, um dieses Problem zu lösen.
Javanio -Kernkomponenten
1.Channel
Konzept
Alle E/A -Operationen in Javanio basieren auf Kanalobjekten, ebenso wie Stream -Operationen auf Stream -Objekten basieren. Daher ist es notwendig, zuerst zu verstehen, was Kanal ist. Der folgende Inhalt wird aus der Dokumentation von JDK1.8 ausgewählt
Achannel repräsentiert die Annex -Verbindung zur Annexität, die sodahardwaredevice, afile, annetworksocket und oroprogrammskomponente, die an einem ormory charakteristischen E/O -Operationen durchgeführt werden kann, das ForexamPlereading Orwriting durchgeführt werden kann.
Aus dem obigen Inhalt können wir feststellen, dass ein Kanal eine Verbindung zu einer bestimmten Entität darstellt, die eine Datei, ein Netzwerkbuchse usw. sein kann. Mit anderen Worten, der Kanal ist eine von Javanio bereitgestellte Brücke, damit unsere Programme mit den zugrunde liegenden E/A -Diensten des Betriebssystems interagieren können.
Kanäle sind eine sehr grundlegende und abstrakte Beschreibung, interagieren mit verschiedenen E/A -Diensten, führen unterschiedliche E/A -Operationen durch und implementieren verschiedene Implementierungen. Daher umfassen die spezifischen FileChannel, Socketchannel usw.
Der Kanal ähnelt dem Stream, wenn es verwendet wird. Es kann Daten in einen Puffer lesen oder Daten in den Puffer in den Kanal schreiben.
Natürlich gibt es auch Unterschiede, die sich hauptsächlich in den folgenden zwei Punkten widerspiegeln:
Ein Kanal kann gelesen und geschrieben werden, während ein Stream ein Weg ist (so unterteilt in InputStream und OutputStream).
Der Kanal hat einen nicht blockierenden E/A-Modus
erreichen
Die am häufigsten verwendeten Kanalimplementierungen in Javanio sind wie folgt, und es ist ersichtlich, dass sie den traditionellen E/A -Betriebsklassen eins nacheinander entsprechen.
Filechannel: Lesen und Schreiben von Dateien
Datagramchannel: UDP -Protokollnetzwerkkommunikation
Socketchannel: TCP -Protokollnetzwerkkommunikation
ServerSocketchannel: Hören Sie TCP -Verbindungen an
2.Buffer
Der in NIO verwendete Puffer ist kein einfaches Byte -Array, sondern eine eingekapselte Pufferklasse. Durch die API, die sie bietet, können wir Daten flexibel manipulieren. Schauen wir uns genauer an.
NiO entspricht Java -Grundtypen und bietet eine Vielzahl von Puffertypen wie ByteBuffer, Charbuffer, IntBuffer usw. Der Unterschied besteht darin, dass die Einheitslänge des Puffer beim Lesen und Schreiben unterschiedlich ist (Lesen und Schreiben in Einheiten der entsprechenden Typvariablen).
Es gibt 3 sehr wichtige Variablen im Puffer. Sie sind der Schlüssel zum Verständnis des Arbeitsmechanismus des Puffers, nämlich
Kapazität (Gesamtkapazität)
Position (die aktuelle Position des Zeigers)
Grenze (Lese-/Schreibgrenzposition)
Die Arbeitsmethode des Puffer ist sehr ähnlich wie Zeichenarrays in C. In der Analogie ist die Kapazität die Gesamtlänge des Arrays, die Position ist die Indexvariable für uns zum Lesen/Schreiben von Zeichen, und die Begrenzung ist die Position des Endcharakters. Die Situation der 3 Variablen zu Beginn des Puffers ist wie folgt
Während des Lesens/Schreibens des Puffers bewegt sich die Position rückwärts und die Grenze ist die Grenze der Positionsbewegung. Es ist nicht schwer vorstellbar, dass beim Schreiben in einen Puffer die Grenze auf die Kapazitätsgröße festgelegt werden sollte und beim Lesen in einem Puffer die Grenze auf die tatsächliche Endposition der Daten festgelegt werden sollte. (Hinweis: Das Schreiben von Pufferdaten in den Kanal ist ein Puffer -Lesevorgang, und das Lesen von Daten vom Kanal zum Puffer ist ein Puffer -Schreibvorgang)
Vor dem Lesen/Schreiben von Vorgängen auf einem Puffer können wir einige Hilfsmethoden aufrufen, die von der Pufferklasse bereitgestellt werden
Flip (): Stellen Sie die Grenze auf den Wert der Position ein und setzen Sie dann die Position auf 0. Aufrufe, bevor Sie den Puffer lesen.
Rewind (): Legen Sie einfach Position 0 fest. Es wird normalerweise vor dem erneuten Lesen der Pufferdaten aufgerufen. Sie werden beispielsweise verwendet, wenn Sie die Daten desselben Puffers lesen und auf mehrere Kanäle schreiben.
Clear (): Kehren Sie zum Ausgangszustand zurück, dh die Grenze entspricht der Kapazität, die Position auf 0. Rufen Sie den Puffer vor dem Schreiben auf.
Compact (): Verschieben Sie ungelesene Daten (Daten zwischen Position und Grenze) auf den Beginn des Puffers und setzen Sie die Position am Ende dieser Daten auf die nächste Position. Tatsächlich ist es gleichwertig, ein solches Daten wieder in den Puffer zu schreiben.
Schauen Sie sich dann ein Beispiel an, verwenden Sie FileChannel, um Textdateien zu lesen und zu schreiben, und überprüfen Sie dieses Beispiel, um die lesbaren und beschreibbaren Merkmale des Kanals und die grundlegende Verwendung von Puffer zu überprüfen (beachten Sie, dass FileCannel nicht auf den nicht blockierenden Modus eingestellt werden kann).
Filechannel Channel = new randomAccessFile ("test.txt", "rw"). GetChannel (); Channel.position (Channel.size ()); // Verschieben Sie den Dateizeiger auf das Ende (anhang schreiben) bytebuffer bytebuffer World!/n ".GetBytes (StandardCharets.utf_8)); // Buffer -> ChannelByTebuffer.flip (); while (bytebuffer.hasremaint ()) {Channel.Write (bytebuffer);} Channel.position (0); // Move die Dateiziner (10) (10) (10) (10) (10) (10) (10) (10) (10) von Anfang an (von Anfang an vorlesen) (von Anfang an vorlesen) (von Anfang an) (von Anfang an vorlesen) (von Anfang an) (von Anfang an) (10), (vorgeschrieben )öckte ( CharSetDeCoder decoder = StandardCharsets.utf_8.NewDeCoder (); // Alle Daten bytebuffer.clear () vorlesen; while (Channel.Read (bytebuffer)! false); System.out.print (charBuffer.flip (). toString ());In diesem Beispiel werden zwei Puffer verwendet, wobei ByteBuffer der Datenpuffer für das Lesen und Schreiben von Kanallern ist, und Charbuffer wird zum Speichern von dekodierten Zeichen verwendet. Die Verwendung von Clear () und Flip () wird wie oben erwähnt. Es ist zu beachten, dass die letzte Compact () -Methode lautet, auch wenn die Größe des Charbuffer vollständig ausreicht, um den dekodierten Datenbytebuffer aufzunehmen, ist dieser Compact () ebenfalls unerlässlich. Dies liegt daran, dass die UTF-8-Codierung von häufig verwendeten chinesischen Zeichen für 3 Bytes berücksichtigt wird. Es besteht daher eine hohe Wahrscheinlichkeit, dass sie in der mittleren Kürzung auftreten wird. Bitte beachten Sie die Abbildung unten:
Wenn der Decoder am Ende des Puffers 0xe4 liest, kann er nicht auf einen Unicode abgebildet werden. Der dritte Parameter der Decode () -Methode, FALS, wird verwendet, um den Decoder die ungünstigen Bytes und die nachfolgenden Daten als zusätzliche Daten zu behandeln. Daher hört die Decode () -Methode hier auf und die Position fällt auf die Position 0xE4 zurück. Auf diese Weise bleibt das erste Byte, das durch das Wort "Medium" codiert wird, im Puffer und muss nach vorne verdichtet und zusammen mit den richtigen und nachfolgenden Sequenzdaten gespleißt werden. In Bezug auf die Charaktercodierung können Sie sich auf " Erklärung von ANSI, Unicode, BMP, UTF und anderen Codierungskonzepten " beziehen.
Übrigens ist der CharSetDecoder im Beispiel auch eine neue Funktion von Javanio, sodass Sie ein wenig entdeckt haben sollten. NIO-Operationen sind pufferorientiert (traditioneller E/A ist streamorientiert).
Zu diesem Zeitpunkt haben wir die grundlegende Verwendung von Kanal und Puffer erfahren. Als nächstes werden wir über wichtige Komponenten darüber sprechen, wie ein Thread mehrere Kanäle verwaltet.
3. Selector
Was ist Selektor
Selector ist eine spezielle Komponente, mit der der Zustand (oder Ereignisse) jedes Kanals gesammelt wird. Wir registrieren den Kanal zuerst in den Selektor und setzen das Ereignis, das uns wichtig ist, und können dann leise warten, bis das Ereignis auftritt, indem wir die Methode Select () aufrufen.
Der Kanal hat die folgenden 4 Ereignisse, die wir anhören können:
Akzeptieren: Es gibt eine akzeptable Verbindung
Verbinden: erfolgreich verbinden
Lesen Sie: Es gibt Daten zum Lesen
Schreiben: Sie können Daten schreiben
Warum Selektor verwenden?
Wie oben erwähnt, müssen Sie, wenn Sie die Blockierung von E/O verwenden, Multi-Thread (eine Speicherverschwendung). Wenn Sie nicht blockierende E/A verwenden, müssen Sie es ständig erneut versuchen (ein CPU-Verbrauch). Die Entstehung des Selektors löst dieses peinliche Problem. Im nicht blockierenden Modus über den Selektor funktionieren unsere Threads nur für Fertigkanäle, und es besteht kein Blind-Versuch. Wenn beispielsweise in allen Kanälen keine Daten erreicht werden, tritt kein Leseereignis auf, und unser Thread wird nach der Methode Select () ausgesetzt, wodurch die CPU -Ressourcen aufgegeben werden.
Wie man benutzt
Erstellen Sie wie unten einen Selektor und registrieren Sie einen Kanal.
Hinweis: Um den Kanal an Selektor zu registrieren, müssen Sie den Kanal zuerst in den Nicht-Blocking-Modus einstellen, andernfalls wird eine Ausnahme geworfen.
Selector selector = selector.open (); Channel.ConfigureBlocking (false); SelectionKey Key = Channel.register (Selektor, SelectionKey.op_read);
Der zweite Parameter der Register () -Methode wird als "Zinssatz" bezeichnet. Dies ist der Ereignissatz, über den Sie besorgt sind. Wenn Sie sich für mehrere Ereignisse interessieren, trennen Sie sie mit einem "Bital oder Operator", z.
SelectionKey.op_read | SelectionKey.op_write
Diese Schreibmethode ist damit nicht vertraut. Es wird in Programmiersprachen gespielt, die Bit -Operationen unterstützen. Die Verwendung einer Ganzzahlvariablen kann mehrere Zustände identifizieren. Wie wird es gemacht? Es ist eigentlich sehr einfach. Zum Beispiel haben einige Konstanten zunächst vordefiniert, und ihre Werte (binär) sind wie folgt
Es kann festgestellt werden, dass die Bits mit ihrem Wert von 1 alle gestaffelt sind, sodass die nach der bitiden Durchführung oder den Berechnungen erhaltenen Werte keine Unklarheit haben und umgekehrt abgeleitet werden können, aus welchen Variablen berechnet werden. Wie man beurteilt, ja, es sind die "Bits and" -Operationen. Beispielsweise gibt es jetzt einen staatlichen variablen Wert von 0011. Wir müssen nur bestimmen, ob der Wert von "0011 & op_read" 1 oder 0 beträgt, um festzustellen, ob das Set den Status OP_READ enthält.
Beachten Sie dann, dass die Register () -Methode ein SelectionKey -Objekt zurückgibt, das die Informationen für diese Registrierung enthält, und wir können die Registrierungsinformationen auch dadurch ändern. Aus dem folgenden vollständigen Beispiel können wir nach Select () sehen, dass wir die Kanäle auch mit dem Zustand fertigstellen, indem wir eine Sammlung von Selektionkeys erhalten.
Ein vollständiges Beispiel
Die Konzepte und theoretischen Dinge wurden erklärt (tatsächlich, nachdem ich sie hier geschrieben habe, stellte ich fest, dass ich nicht viel geschrieben habe, was so peinlich ist (⊙ˍ⊙)). Schauen wir uns ein komplettes Beispiel an.
In diesem Beispiel wird Javanio verwendet, um einen Single-Thread-Server zu implementieren. Die Funktion ist sehr einfach. Es hört auf die Client -Verbindung. Wenn die Verbindung hergestellt wird, liest sie die Nachricht des Kunden und antwortet auf eine Nachricht an den Client.
Es ist zu beachten, dass ich das Zeichen '/0' (ein Byte mit einem Wert von 0) verwende, um das Ende der Nachricht zu identifizieren.
Einzelner Thread -Server
public class nioserver {public static void main (String [] args) löst IOException {// Erstellen Sie einen SelektorSelector selector = selector.open (); // Initialisieren Sie die TCP -Verbindungshörkanal -ServerSocketchannel -ListendEnchannel = ServerSocketchannel.open (); Listenchannel.bind (New Inetsocketaddress (9999)); listenchannel.configureBlocking (false); // Registrieren Sie sich bei Selektor (hören Sie sich das Akzeptierenereignis an) Listenchannel.register (Selector, SelectionKey.op_accept); // Erstellen Sie einen Buffer -Bytebuffer Buffer = byteBuffer.Alldocate (100); while (true) {selector.select (); // Block, bis ein Ereignis auftritt Iterator <SelectionKey> keyiter = selector.selectedkeys (). Iterator (); // Zugriff auf das Kanalereignis, das durch einen Iterator von einem Iterator ausgewählt wurde, while while while (keyiter.hasnext ()) {selectionKey = keyRection.Next (); Socketchannel Channel = ((ServerSocketchannel) key.channel ()). Accept (); Channel.ConfigureBlocking (false); Channel.register (Selector, SelectionKey.op_read ).OUT.OUT.PRINTLN ("Verbindung mit [" + Channel.getRemoteDresse () + "]!"). Buffer.clear (); // Lesen am Ende des Streams, was darauf hinweist, dass die TCP -Verbindung nicht verbunden wurde. puffer.flip (); while (buffer.hasremaining ()) {byte b = buffer.get (); if (b == 0) {// /0System.out.println () am Ende der Client -Nachricht; // Antwort client puffer.clear (); buffer.put ("hello, client!/0". {((Socketchannel) key.channel ()). Write (Buffer);}} else {System.out.print ((char) b);}}} // Für Ereignisse, die verarbeitet wurden, müssen Sie keyiter.remove () manuell entfernen.Kunde
Dieser Client wird nur zum Testen verwendet. Um es weniger schwierig zu machen, verwendet es traditionelle Schreibmethoden und der Code ist sehr kurz.
Wenn Sie im Testen strenger sein müssen, sollten Sie eine große Anzahl von Clients gleichzeitig ausführen, um die Antwortzeit des Servers zu zählen, und senden keine Daten unmittelbar nach der Erstellung der Verbindung, um den Vorteilen der nicht blockierenden E/A auf dem Server vollständige Spiele zu verleihen.
Public Class Client {public static void main (String [] args) löst eine Ausnahme aus {Socket Socket = new Socket ("localhost", 9999); InputStream is = Socket.GetInputStream (); outputStream os = socket.getOutputStream (); // Daten an den ersten os.write des Servers senden. while ((b = is.read ())!Zusammenfassen
In der obigen Stelle geht es um ein schnelles Verständnis der NIO -Kernkomponenten in Java. Ich hoffe, es wird für alle hilfreich sein. Interessierte Freunde können weiterhin auf andere relevante Inhalte dieser Website verweisen. Wenn es Mängel gibt, hinterlassen Sie bitte eine Nachricht, um darauf hinzuweisen. Vielen Dank an Freunde für Ihre Unterstützung für diese Seite!