Java NIO bietet eine andere Art der IO-Arbeitsweise als Standard-IO:
Kanäle und Puffer: Standard-IO arbeitet auf der Grundlage von Byteströmen und Zeichenströmen, während NIO auf der Grundlage von Kanälen (Kanal) und Puffern (Puffer) arbeitet. Daten werden immer vom Kanal in den Pufferbereich gelesen oder vom Puffer in den Pufferbereich geschrieben Kanal.
Asynchrones IO: Mit Java NIO können Sie IO asynchron verwenden. Wenn ein Thread beispielsweise Daten aus einem Kanal in einen Puffer liest, kann der Thread weiterhin andere Dinge tun. Wenn Daten in den Puffer geschrieben werden, kann der Thread sie weiter verarbeiten. Das Schreiben aus einem Puffer auf einen Kanal ist ähnlich.
Selektoren: Java NIO führt das Konzept von Selektoren ein, die zum Abhören von Ereignissen auf mehreren Kanälen verwendet werden (z. B. Verbindungsöffnung, Datenankunft). Daher kann ein einzelner Thread mehrere Datenkanäle abhören.
Lassen Sie uns die relevanten Kenntnisse von Java NIO im Detail vorstellen.
Java NIO-Übersicht
Java NIO besteht aus den folgenden Kernteilen:
Kanäle
Puffer
Selektoren
Obwohl es in Java NIO viele andere Klassen und Komponenten gibt, bilden meiner Meinung nach Channel, Buffer und Selector die Kern-API. Andere Komponenten wie Pipe und FileLock sind lediglich Dienstprogrammklassen, die mit den drei Kernkomponenten verwendet werden. Daher werde ich mich in dieser Übersicht auf diese drei Komponenten konzentrieren. Andere Komponenten werden in separaten Kapiteln behandelt.
Kanal und Puffer
Grundsätzlich beginnen alle E/A in NIO von einem Kanal. Kanäle sind ein bisschen wie Streams. Daten können vom Kanal in den Puffer gelesen oder vom Puffer in den Kanal geschrieben werden. Hier ist eine Illustration:
Es gibt verschiedene Arten von Kanälen und Puffern. Im Folgenden sind die Implementierungen einiger Hauptkanäle in JAVA NIO aufgeführt:
FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel
Wie Sie sehen, decken diese Kanäle UDP- und TCP-Netzwerk-IO sowie Datei-IO ab.
Neben diesen Klassen gibt es einige interessante Schnittstellen, der Einfachheit halber habe ich jedoch versucht, sie in der Übersicht nicht zu erwähnen. Ich werde sie in anderen Kapiteln dieses Tutorials erläutern, sofern sie relevant sind.
Das Folgende ist die wichtigste Pufferimplementierung in Java NIO:
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
Diese Puffer decken die grundlegenden Datentypen ab, die Sie über IO senden können: Byte, Short, Int, Long, Float, Double und Char.
Java NIO verfügt auch über einen Mappyteuffer, der zur Darstellung von speicherzugeordneten Dateien verwendet wird. Ich werde ihn in der Übersicht nicht erklären.
Wähler
Mit Selector kann ein einzelner Thread mehrere Kanäle verarbeiten. Wenn Ihre Anwendung mehrere Verbindungen (Kanäle) öffnet, der Datenverkehr jeder Verbindung jedoch sehr gering ist, kann die Verwendung von Selector praktisch sein. Zum Beispiel in einem Chatserver.
Dies ist eine Veranschaulichung der Verwendung eines Selektors zur Verarbeitung von 3 Kanälen in einem einzelnen Thread:
Um einen Selektor zu verwenden, müssen Sie einen Kanal beim Selektor registrieren und dann seine Methode select() aufrufen. Diese Methode blockiert, bis für einen registrierten Kanal ein Ereignis bereit ist. Sobald diese Methode zurückkehrt, kann der Thread diese Ereignisse verarbeiten. Beispiele für Ereignisse sind neu eingehende Verbindungen, Datenempfang usw.
Java NIO vs. IO
(Originaladresse dieses Teils, Autor: Jakob Jenkov, Übersetzer: Guo Lei, Korrekturleser: Fang Tengfei)
Nachdem ich etwas über Java NIO und IO API gelernt hatte, kam mir sofort eine Frage in den Sinn:
Zitat
Wann sollte ich IO und wann NIO verwenden? In diesem Artikel werde ich versuchen, die Unterschiede zwischen Java NIO und IO, ihre Verwendungsszenarien und wie sie sich auf Ihr Code-Design auswirken, klar zu erklären.
Hauptunterschiede zwischen Java NIO und IO
Die folgende Tabelle fasst die Hauptunterschiede zwischen Java NIO und IO zusammen. Ich werde die Unterschiede in jedem Teil der Tabelle detaillierter beschreiben.
IO NIO
Stream-orientiert, pufferorientiert
Blockierende E/A. Nicht blockierende E/A
Selektoren
Streamorientiert und pufferorientiert
Der erste große Unterschied zwischen Java NIO und IO besteht darin, dass IO streamorientiert und NIO pufferorientiert ist. Java IO ist Stream-orientiert, was bedeutet, dass ein oder mehrere Bytes gleichzeitig aus dem Stream gelesen werden und bis alle Bytes gelesen sind, werden sie nirgendwo zwischengespeichert. Darüber hinaus können Daten im Stream nicht vorwärts oder rückwärts verschoben werden. Wenn Sie die aus dem Stream gelesenen Daten hin und her verschieben müssen, müssen Sie sie zunächst in einem Puffer zwischenspeichern. Der pufferorientierte Ansatz von Java NIO ist etwas anders. Die Daten werden in einen Puffer eingelesen, den sie später verarbeiten und bei Bedarf im Puffer hin und her bewegen. Dies erhöht die Flexibilität in der Verarbeitung. Sie müssen jedoch auch überprüfen, ob der Puffer alle Daten enthält, die Sie verarbeiten müssen. Stellen Sie außerdem sicher, dass unverarbeitete Daten im Puffer nicht überschrieben werden, wenn mehr Daten in den Puffer eingelesen werden.
Blockierende und nicht blockierende E/A
Verschiedene Streams von Java IO blockieren. Das bedeutet, dass, wenn ein Thread read() oder write() aufruft, der Thread blockiert wird, bis einige Daten gelesen oder die Daten vollständig geschrieben wurden. Während dieser Zeit kann der Thread nichts anderes tun. Der nicht blockierende Modus von Java NIO ermöglicht es einem Thread, eine Anfrage zum Lesen von Daten von einem bestimmten Kanal zu senden, kann jedoch nur die aktuell verfügbaren Daten abrufen. Wenn derzeit keine Daten verfügbar sind, werden keine Daten abgerufen. Anstatt den Thread blockiert zu halten, kann der Thread weiterhin andere Dinge tun, bis die Daten lesbar werden. Das Gleiche gilt für nicht blockierende Schreibvorgänge. Ein Thread fordert das Schreiben einiger Daten in einen Kanal an, muss jedoch nicht warten, bis diese vollständig geschrieben sind. Der Thread kann in der Zwischenzeit andere Dinge tun. Threads nutzen normalerweise Leerlaufzeit bei nicht blockierendem E/A, um E/A-Vorgänge auf anderen Kanälen auszuführen, sodass ein einzelner Thread jetzt mehrere Eingabe- und Ausgabekanäle verwalten kann.
Selektoren
Mit den Selektoren von Java NIO kann ein einzelner Thread mehrere Kanäle mit einem Selektor überwachen und dann einen separaten Thread zum „Auswählen“ von Kanälen verwenden: Diese Kanäle verfügen bereits über Eingaben, die verarbeitet werden können bereit zum Schreiben. Dieser Auswahlmechanismus erleichtert einem einzelnen Thread die Verwaltung mehrerer Kanäle.
Wie sich NIO und IO auf das Anwendungsdesign auswirken
Unabhängig davon, ob Sie sich für eine IO- oder NIO-Toolbox entscheiden, gibt es mehrere Aspekte, die sich auf Ihr Anwendungsdesign auswirken können:
API-Aufrufe an NIO- oder IO-Klassen.
Datenverarbeitung.
Die Anzahl der Threads, die zur Datenverarbeitung verwendet werden.
API-Aufruf
Natürlich sehen die API-Aufrufe bei der Verwendung von NIO anders aus als bei der Verwendung von IO, aber das ist nicht unerwartet, denn anstatt nur Byte für Byte aus einem InputStream zu lesen, müssen die Daten zunächst in einen Puffer eingelesen und dann verarbeitet werden.
Datenverarbeitung
Bei Verwendung des reinen NIO-Designs im Vergleich zum IO-Design ist auch die Datenverarbeitung betroffen.
Beim IO-Design lesen wir Daten Byte für Byte aus InputStream oder Reader. Angenommen, Sie verarbeiten einen zeilenbasierten Textdatenstrom, zum Beispiel:
Kopieren Sie den Codecode wie folgt:
Name: Anna
Alter: 25
E-Mail: [email protected]
Telefon: 1234567890
Der Textzeilenstrom kann wie folgt gehandhabt werden:
Kopieren Sie den Codecode wie folgt:
InputStream input = … ; // den InputStream vom Client-Socket abrufen
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
String nameLine = reader.readLine();
String ageLine = reader.readLine();
String emailLine= reader.readLine();
String phoneLine= reader.readLine();
Beachten Sie, dass der Verarbeitungsstatus davon abhängt, wie lange das Programm ausgeführt wurde. Mit anderen Worten: Sobald die Methode „reader.readLine()“ zurückkehrt, wissen Sie sicher, dass die Textzeile gelesen wurde. Aus diesem Grund blockiert readline(), bis die gesamte Zeile gelesen wurde. Sie wissen auch, dass diese Zeile Namen enthält. Wenn der zweite readline()-Aufruf zurückkehrt, wissen Sie auch, dass diese Zeile Altersangaben usw. enthält. Wie Sie sehen, wird dieser Handler nur ausgeführt, wenn neue Daten eingelesen werden, und weiß bei jedem Schritt, um welche Daten es sich handelt. Sobald ein laufender Thread einige der gelesenen Daten verarbeitet hat, wird er die Daten (meistens) nicht zurücksetzen. Auch die folgende Abbildung verdeutlicht dieses Prinzip:
Daten aus einem blockierten Stream lesen
Während eine NIO-Implementierung anders sein wird, ist hier ein einfaches Beispiel:
Kopieren Sie den Codecode wie folgt:
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
Beachten Sie die zweite Zeile, in der Bytes aus dem Kanal in einen ByteBuffer gelesen werden. Wenn dieser Methodenaufruf zurückkehrt, wissen Sie nicht, ob sich alle benötigten Daten im Puffer befinden. Sie wissen lediglich, dass der Puffer einige Bytes enthält, was die Verarbeitung etwas erschwert.
Angenommen, nach dem ersten Leseaufruf (Puffer) sind die in den Puffer eingelesenen Daten nur eine halbe Zeile, z. B. „Name: An“. Können Sie die Daten verarbeiten? Offensichtlich nicht, Sie müssen warten, bis die gesamte Datenzeile in den Cache eingelesen wurde. Davor ist jede Verarbeitung der Daten bedeutungslos.
Woher wissen Sie also, ob der Puffer genügend Daten zur Verarbeitung enthält? Nun, Sie wissen es nicht. Erkannte Methoden können nur Daten im Puffer anzeigen. Dies führt dazu, dass Sie die Daten des Puffers mehrmals überprüfen müssen, bevor Sie wissen, dass sich alle Daten im Puffer befinden. Dies ist nicht nur ineffizient, sondern kann auch die Programmierlösung überladen. Zum Beispiel:
Kopieren Sie den Codecode wie folgt:
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
while(! bufferFull(bytesRead) ) {
bytesRead = inChannel.read(buffer);
}
Die Methode bufferFull() muss verfolgen, wie viele Daten in den Puffer eingelesen wurden, und je nachdem, ob der Puffer voll ist, true oder false zurückgeben. Mit anderen Worten: Wenn der Puffer zur Verarbeitung bereit ist, ist er voll.
Die Methode bufferFull() durchsucht den Puffer, muss jedoch im gleichen Zustand bleiben wie vor dem Aufruf der Methode bufferFull(). Andernfalls werden die nächsten in den Puffer eingelesenen Daten möglicherweise nicht an der richtigen Stelle gelesen. Das ist unmöglich, aber es ist ein weiteres Problem, dessen man sich bewusst sein muss.
Wenn der Puffer voll ist, kann er verarbeitet werden. Wenn es nicht funktioniert und es in Ihrem konkreten Fall sinnvoll ist, können Sie möglicherweise einiges davon in den Griff bekommen. Doch in vielen Fällen ist dies nicht der Fall. Die folgende Abbildung zeigt „Pufferdatenzyklus bereit“:
Liest Daten von einem Kanal, bis alle Daten in den Puffer gelesen wurden
Zusammenfassen
Mit NIO können Sie mehrere Kanäle (Netzwerkverbindungen oder Dateien) mit nur einem einzigen Thread (oder einigen wenigen) verwalten. Der Nachteil besteht jedoch darin, dass das Parsen der Daten komplexer sein kann als das Lesen aus einem blockierenden Stream.
Wenn Sie Tausende von gleichzeitig geöffneten Verbindungen verwalten müssen, die jedes Mal nur kleine Datenmengen senden, wie z. B. einen Chat-Server, kann ein Server, der NIO implementiert, von Vorteil sein. Wenn Sie viele offene Verbindungen zu anderen Computern aufrechterhalten müssen, beispielsweise in einem P2P-Netzwerk, kann es ebenfalls von Vorteil sein, einen separaten Thread für die Verwaltung aller ausgehenden Verbindungen zu verwenden. Das Entwurfsschema mehrerer Verbindungen in einem Thread ist in der folgenden Abbildung dargestellt:
Ein einzelner Thread verwaltet mehrere Verbindungen
Wenn Sie über eine kleine Anzahl von Verbindungen verfügen, die eine sehr hohe Bandbreite nutzen und große Datenmengen auf einmal senden, könnte eine typische IO-Server-Implementierung möglicherweise eine gute Lösung sein. Die folgende Abbildung zeigt ein typisches IO-Server-Design:
Ein typisches IO-Server-Design:
Eine Verbindung wird von einem Thread verwaltet
Kanal
Java NIO-Kanäle ähneln Streams, unterscheiden sich jedoch etwas:
Daten können vom Kanal gelesen und Daten in den Kanal geschrieben werden. Aber Lese- und Schreibströme sind normalerweise einseitig.
Kanäle können asynchron gelesen und geschrieben werden.
Daten im Kanal müssen zuerst aus einem Puffer gelesen oder immer aus einem Puffer geschrieben werden.
Wie oben erwähnt, werden Daten vom Kanal in den Puffer gelesen und Daten vom Puffer in den Kanal geschrieben. Wie unten gezeigt:
Kanalimplementierung
Dies sind die Implementierungen der wichtigsten Kanäle in Java NIO:
FileChannel: Daten aus Dateien lesen und schreiben.
DatagramChannel: Kann Daten im Netzwerk über UDP lesen und schreiben.
SocketChannel: Kann Daten im Netzwerk über TCP lesen und schreiben.
ServerSocketChannel: Kann eingehende TCP-Verbindungen überwachen, wie ein Webserver. Für jede neue eingehende Verbindung wird ein SocketChannel erstellt.
Beispiel für einen einfachen Kanal
Das Folgende ist ein Beispiel für die Verwendung von FileChannel zum Einlesen von Daten in einen Puffer:
Kopieren Sie den Codecode wie folgt:
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("Read " + bytesRead);
buf.flip();
while(buf.hasRemaining()){
System.out.print((char) buf.get());
}
buf.clear();
bytesRead = inChannel.read(buf);
}
aFile.close();
Beachten Sie, dass der Aufruf von buf.flip() zuerst Daten in den Puffer liest, dann den Puffer umkehrt und dann die Daten aus dem Puffer liest. Im nächsten Abschnitt wird näher auf Buffer eingegangen.
Puffer
Puffer in Java NIO wird zur Interaktion mit NIO-Kanälen verwendet. Wie Sie wissen, werden Daten vom Kanal in den Puffer gelesen und vom Puffer in den Kanal geschrieben.
Ein Puffer ist im Wesentlichen ein Speicherblock, in den Daten geschrieben und aus dem dann Daten gelesen werden können. Dieser Speicher ist als NIO-Pufferobjekt verpackt und bietet eine Reihe von Methoden für den bequemen Zugriff auf diesen Speicher.
Grundlegende Verwendung von Buffer
Die Verwendung von Buffer zum Lesen und Schreiben von Daten erfolgt im Allgemeinen in den folgenden vier Schritten:
Daten in den Puffer schreiben
Rufen Sie die Methode flip() auf
Daten aus dem Puffer lesen
Rufen Sie die Methode „clear()“ oder „compact()“ auf
Wenn Daten in den Puffer geschrieben werden, zeichnet der Puffer auf, wie viele Daten geschrieben wurden. Sobald Sie Daten lesen möchten, müssen Sie den Puffer über die Methode flip() vom Schreibmodus in den Lesemodus umschalten. Im Lesemodus können alle zuvor in den Puffer geschriebenen Daten gelesen werden.
Nachdem alle Daten gelesen wurden, muss der Puffer geleert werden, damit er erneut beschrieben werden kann. Es gibt zwei Möglichkeiten, den Puffer zu löschen: Aufruf der Methode clear() oder compact(). Die Methode clear() löscht den gesamten Puffer. Die Methode compact() löscht nur die gelesenen Daten. Alle ungelesenen Daten werden an den Anfang des Puffers verschoben und neu geschriebene Daten werden nach den ungelesenen Daten im Puffer platziert.
Hier ist ein Beispiel für die Verwendung von Buffer:
Kopieren Sie den Codecode wie folgt:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
//Puffer mit einer Kapazität von 48 Bytes erstellen
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf); //in den Puffer lesen.
while (bytesRead != -1) {
buf.flip();//Puffer zum Lesen bereit machen
while(buf.hasRemaining()){
System.out.print((char) buf.get()); // jeweils 1 Byte lesen
}
buf.clear(); //Puffer zum Schreiben bereit machen
bytesRead = inChannel.read(buf);
}
aFile.close();
Kapazität, Position und Grenze des Puffers
Ein Puffer ist im Wesentlichen ein Speicherblock, in den Daten geschrieben und aus dem dann Daten gelesen werden können. Dieser Speicher ist als NIO-Pufferobjekt verpackt und bietet eine Reihe von Methoden für den bequemen Zugriff auf diesen Speicher.
Um zu verstehen, wie Buffer funktioniert, müssen Sie mit seinen drei Eigenschaften vertraut sein:
Kapazität
Position
Limit
Die Bedeutung von Position und Grenze hängt davon ab, ob sich der Puffer im Lesemodus oder im Schreibmodus befindet. Unabhängig davon, in welchem Modus sich der Puffer befindet, ist die Bedeutung der Kapazität immer dieselbe.
Hier finden Sie eine Erläuterung der Kapazität, Position und Grenze im Lese- und Schreibmodus, mit detaillierten Erläuterungen im Anschluss an die Abbildung.
Kapazität
Als Speicherblock hat der Puffer einen festen Größenwert, der auch „Kapazität“ genannt wird. Es können nur Kapazitäten vom Typ „Byte“, „Long“, „Char“ und andere Typen in ihn geschrieben werden. Sobald der Puffer voll ist, muss er geleert werden (durch Lesen von Daten oder Löschen von Daten), bevor mit dem Schreiben von Daten fortgefahren werden kann.
Position
Wenn Sie Daten in den Puffer schreiben, stellt die Position die aktuelle Position dar. Der anfängliche Positionswert ist 0. Wenn Byte-, Langdaten usw. in den Puffer geschrieben werden, wird die Position zur nächsten Puffereinheit verschoben, wo Daten eingefügt werden können. Die maximale Position kann Kapazität 1 sein.
Wenn Daten gelesen werden, werden sie auch von einem bestimmten Ort gelesen. Beim Umschalten des Puffers vom Schreibmodus in den Lesemodus wird die Position auf 0 zurückgesetzt. Wenn Daten von der Position des Puffers gelesen werden, bewegt sich die Position vorwärts zur nächsten lesbaren Position.
Limit
Im Schreibmodus gibt die Grenze des Puffers die maximale Datenmenge an, die Sie in den Puffer schreiben können. Im Schreibmodus entspricht die Grenze der Kapazität des Puffers.
Wenn Sie den Puffer in den Lesemodus schalten, gibt „Limit“ die maximale Datenmenge an, die Sie lesen können. Daher wird beim Umschalten des Puffers in den Lesemodus der Grenzwert auf den Positionswert im Schreibmodus gesetzt. Mit anderen Worten, Sie können alle zuvor geschriebenen Daten lesen (die Anzahl der geschriebenen Daten ist begrenzt, dieser Wert ist die Position im Schreibmodus).
Puffertyp
Java NIO verfügt über die folgenden Puffertypen:
ByteBuffer
MappedByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
Wie Sie sehen, repräsentieren diese Puffertypen unterschiedliche Datentypen. Mit anderen Worten, die Bytes im Puffer können durch die Typen char, short, int, long, float oder double manipuliert werden.
MappedByteBuffer ist etwas Besonderes und wird in einem eigenen Kapitel besprochen.
Pufferzuweisung
Um ein Buffer-Objekt zu erhalten, müssen Sie es zunächst zuweisen. Jede Buffer-Klasse verfügt über eine Allocate-Methode. Unten finden Sie ein Beispiel für die Zuweisung eines ByteBuffer mit 48 Byte Kapazität.
Kopieren Sie den Codecode wie folgt:
ByteBuffer buf = ByteBuffer.allocate(48);
Dadurch wird ein CharBuffer zugewiesen, der 1024 Zeichen speichern kann:
Kopieren Sie den Codecode wie folgt:
CharBuffer buf = CharBuffer.allocate(1024);
Daten in den Puffer schreiben
Es gibt zwei Möglichkeiten, Daten in den Puffer zu schreiben:
Vom Kanal in den Puffer schreiben.
Schreiben Sie über die put()-Methode von Buffer in den Puffer.
Beispiel für das Schreiben vom Kanal in den Puffer
Kopieren Sie den Codecode wie folgt:
int bytesRead = inChannel.read(buf); //in den Puffer lesen.
Beispiel für das Schreiben der Buffer-Through-Put-Methode:
Kopieren Sie den Codecode wie folgt:
buf.put(127);
Es gibt viele Versionen der Put-Methode, mit denen Sie Daten auf unterschiedliche Weise in den Puffer schreiben können. Beispielsweise das Schreiben an einen angegebenen Speicherort oder das Schreiben eines Byte-Arrays in einen Puffer. Weitere Einzelheiten zur Pufferimplementierung finden Sie in JavaDoc.
flip()-Methode
Die Flip-Methode schaltet den Puffer vom Schreibmodus in den Lesemodus. Durch Aufrufen der Methode flip() wird die Position auf 0 zurückgesetzt und das Limit auf den Wert der vorherigen Position gesetzt.
Mit anderen Worten: „Position“ wird jetzt verwendet, um die Leseposition zu markieren, und „Limit“ stellt dar, wie viele Bytes, Zeichen usw. zuvor geschrieben wurden – wie viele Bytes, Zeichen usw. jetzt gelesen werden können.
Daten aus dem Puffer lesen
Es gibt zwei Möglichkeiten, Daten aus dem Puffer zu lesen:
Daten vom Puffer zum Kanal lesen.
Verwenden Sie die Methode get(), um Daten aus dem Puffer zu lesen.
Beispiel für das Lesen von Daten vom Puffer zum Kanal:
Kopieren Sie den Codecode wie folgt:
//vom Puffer in den Kanal lesen.
int bytesWritten = inChannel.write(buf);
Beispiel für die Verwendung der get()-Methode zum Lesen von Daten aus dem Puffer
Kopieren Sie den Codecode wie folgt:
Byte aByte = buf.get();
Es gibt viele Versionen der get-Methode, mit denen Sie Daten auf unterschiedliche Weise aus dem Puffer lesen können. Lesen Sie beispielsweise von einer bestimmten Position aus oder lesen Sie Daten aus einem Puffer in ein Byte-Array. Weitere Einzelheiten zur Pufferimplementierung finden Sie in JavaDoc.
rewind()-Methode
Buffer.rewind() setzt die Position auf 0 zurück, sodass Sie alle Daten im Puffer erneut lesen können. Das Limit bleibt unverändert und gibt weiterhin an, wie viele Elemente (Byte, Zeichen usw.) aus dem Puffer gelesen werden können.
Clear()- und Compact()-Methoden
Sobald die Daten im Puffer gelesen wurden, muss der Puffer bereit sein, erneut beschrieben zu werden. Dies kann über die Methoden „clear()“ oder „compact()“ erfolgen.
Wenn die Methode clear() aufgerufen wird, wird die Position auf 0 zurückgesetzt und das Limit auf den Wert der Kapazität gesetzt. Mit anderen Worten: Der Puffer wird gelöscht. Die Daten im Puffer werden nicht gelöscht, aber diese Markierungen sagen uns, wo wir mit dem Schreiben von Daten in den Puffer beginnen sollen.
Wenn sich im Puffer ungelesene Daten befinden und Sie die Methode clear() aufrufen, werden die Daten „vergessen“, was bedeutet, dass es keine Markierungen mehr gibt, die Ihnen sagen, welche Daten gelesen wurden und welche nicht.
Wenn sich noch ungelesene Daten im Puffer befinden und die Daten später benötigt werden, Sie aber zuerst einige Daten schreiben möchten, verwenden Sie die Methode compact().
Die Methode compact() kopiert alle ungelesenen Daten an den Anfang des Puffers. Stellen Sie dann die Position direkt hinter dem letzten ungelesenen Element ein. Das Limit-Attribut ist weiterhin wie die Methode clear() auf Kapazität gesetzt. Der Puffer ist jetzt zum Schreiben von Daten bereit, ungelesene Daten werden jedoch nicht überschrieben.
mark()- und reset()-Methoden
Durch Aufrufen der Methode Buffer.mark() können Sie eine bestimmte Position im Puffer markieren. Sie können diese Position später wiederherstellen, indem Sie die Methode Buffer.reset() aufrufen. Zum Beispiel:
Kopieren Sie den Codecode wie folgt:
buffer.mark();
//buffer.get() ein paar Mal aufrufen, z. B. während des Parsens.
buffer.reset();//Position auf Markierung zurücksetzen.
Methoden „equals()“ und „compareTo()“.
Sie können die Methoden „equals()“ und „compareTo()“ für zwei Puffer verwenden.
gleich()
Wenn die folgenden Bedingungen erfüllt sind, bedeutet dies, dass die beiden Puffer gleich sind:
Haben den gleichen Typ (Byte, Char, Int usw.).
Die Anzahl der verbleibenden Bytes, Zeichen usw. im Puffer ist gleich.
Alle verbleibenden Bytes, Zeichen usw. im Puffer sind gleich.
Wie Sie sehen können, vergleicht „equals“ nur einen Teil des Puffers, nicht jedes Element darin. Tatsächlich werden nur die verbleibenden Elemente im Puffer verglichen.
CompareTo()-Methode
Die Methode „compareTo()“ vergleicht die verbleibenden Elemente (Byte, Zeichen usw.) von zwei Puffern. Wenn die folgenden Bedingungen erfüllt sind, gilt ein Puffer als „kleiner als“ der andere Puffer:
Das erste ungleiche Element ist kleiner als das entsprechende Element im anderen Puffer.
Alle Elemente sind gleich, aber der erste Puffer ist vor dem anderen erschöpft (der erste Puffer enthält weniger Elemente als der andere).
(Anmerkung: Die übrigen Elemente sind die Elemente von Position bis Grenze)
Streuen/Sammeln
(Originaladresse dieses Teils, Autor: Jakob Jenkov, Übersetzer: Guo Lei)
Java NIO beginnt mit der Unterstützung von Scatter/Gather. Scatter/Gather wird verwendet, um den Vorgang des Lesens oder Schreibens in einen Kanal zu beschreiben (Anmerkung des Übersetzers: Kanal wird auf Chinesisch oft als Kanal übersetzt).
Streulesen vom Kanal bedeutet, dass die gelesenen Daten während des Lesevorgangs in mehrere Puffer geschrieben werden. Daher „streut“ der Kanal die vom Kanal gelesenen Daten auf mehrere Puffer.
Das Sammeln und Schreiben in einen Kanal bedeutet, dass während eines Schreibvorgangs Daten aus mehreren Puffern in denselben Kanal geschrieben werden. Daher „sammelt“ der Kanal die Daten in mehreren Puffern und sendet sie an den Kanal.
Scatter/Gather wird häufig in Situationen verwendet, in denen die übertragenen Daten separat verarbeitet werden müssen. Wenn Sie beispielsweise eine Nachricht übertragen, die aus einem Nachrichtenkopf und einem Nachrichtenhauptteil besteht, können Sie den Nachrichtenhauptteil und den Nachrichtenkopf auf verschiedene Puffer verteilen Sie können Nachrichtenkopfzeilen und Nachrichtentexte bequem verarbeiten.
Streuende Lesevorgänge
Unter Scattering Reads versteht man das Lesen von Daten von einem Kanal in mehrere Puffer. Wie in der folgenden Abbildung beschrieben:
Das Codebeispiel lautet wie folgt:
Kopieren Sie den Codecode wie folgt:
ByteBuffer-Header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
Kanal.read(bufferArray);
Beachten Sie, dass der Puffer zuerst in das Array eingefügt wird und das Array dann als Eingabeparameter für channel.read() verwendet wird. Die Methode read() schreibt die vom Kanal gelesenen Daten in der Reihenfolge des Puffers im Array in den Puffer. Wenn ein Puffer gefüllt ist, schreibt der Kanal in einen anderen Puffer.
Scattering Reads müssen den aktuellen Puffer füllen, bevor sie zum nächsten Puffer wechseln, was auch bedeutet, dass sie nicht für dynamische Nachrichten geeignet sind (Anmerkung des Übersetzers: Die Nachrichtengröße ist nicht festgelegt). Mit anderen Worten: Wenn ein Nachrichtenheader und ein Nachrichtentext vorhanden sind, muss der Nachrichtenheader vollständig ausgefüllt sein (z. B. 128 Byte), damit Scattering Reads ordnungsgemäß funktioniert.
Sammeln von Schriften
Beim Sammeln von Schreibvorgängen werden Daten aus mehreren Puffern in denselben Kanal geschrieben. Wie in der folgenden Abbildung beschrieben:
Das Codebeispiel lautet wie folgt:
Kopieren Sie den Codecode wie folgt:
ByteBuffer-Header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
//Daten in Puffer schreiben
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);
Das buffers-Array ist der Eingabeparameter der write()-Methode. Die write()-Methode schreibt Daten in der Reihenfolge der Puffer im Array. Wenn also ein Puffer eine Kapazität von 128 Byte hat, aber nur 58 Byte Daten enthält, werden die 58 Byte Daten in den Kanal geschrieben. Daher können Gathering Writes im Gegensatz zu Scattering Reads dynamische Nachrichten besser verarbeiten.
Datenübertragung zwischen Kanälen
(Originaladresse dieses Teils, Autor: Jakob Jenkov, Übersetzer: Guo Lei, Korrekturleser: Zhou Tai)
Wenn in Java NIO einer der beiden Kanäle ein FileChannel ist, können Sie Daten direkt von einem Kanal (Anmerkung des Übersetzers: Kanal wird auf Chinesisch oft als Kanal übersetzt) auf einen anderen Kanal übertragen.
transferFrom()
Die transferFrom()-Methode von FileChannel kann Daten vom Quellkanal zum FileChannel übertragen (Anmerkung des Übersetzers: Diese Methode wird in der JDK-Dokumentation als Übertragung von Bytes von einem bestimmten lesbaren Byte-Kanal in die Datei dieses Kanals erklärt.). Hier ist ein einfaches Beispiel:
Kopieren Sie den Codecode wie folgt:
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
Long-Position = 0;
long count = fromChannel.size();
toChannel.transferFrom(position, count, fromChannel);
Der Eingabeparameter position der Methode gibt an, ab welcher Position Daten in die Zieldatei geschrieben werden sollen, und count gibt die maximale Anzahl übertragener Bytes an. Wenn der Quellkanal weniger als 50 Bytes verbleibenden Speicherplatz hat, ist die Anzahl der übertragenen Bytes geringer als die Anzahl der angeforderten Bytes.
Darüber hinaus ist zu beachten, dass SocketChannel bei der Implementierung von SoketChannel nur die zu diesem Zeitpunkt vorbereiteten Daten überträgt (die möglicherweise weniger als die Anzahl der Bytes betragen). Daher überträgt der SocketChannel möglicherweise nicht alle angeforderten Daten (Anzahl der Bytes) in den FileChannel.
transferTo()
Die Methode transferTo() überträgt Daten von FileChannel an andere Kanäle. Hier ist ein einfaches Beispiel:
Kopieren Sie den Codecode wie folgt:
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
Long-Position = 0;
long count = fromChannel.size();
fromChannel.transferTo(position, count, toChannel);
Haben Sie festgestellt, dass dieses Beispiel dem vorherigen besonders ähnlich ist? Abgesehen davon, dass das FileChannel-Objekt, das die Methode aufruft, unterschiedlich ist, ist alles andere gleich.
Die oben erwähnten Probleme zu SocketChannel bestehen auch bei der Methode transferTo(). SocketChannel überträgt weiterhin Daten, bis der Zielpuffer gefüllt ist.
Wähler
(Link zum Originaltext dieses Abschnitts, Autor: Jakob Jenkov, Übersetzer: Langjiv, Korrekturleser: Ding Yi)
Selector ist eine Komponente in Java NIO, die einen oder mehrere NIO-Kanäle erkennen und wissen kann, ob der Kanal für Ereignisse wie Lesen und Schreiben bereit ist. Auf diese Weise kann ein einzelner Thread mehrere Kanäle und damit mehrere Netzwerkverbindungen verwalten.
(1)Warum Selector verwenden?
Der Vorteil der Verwendung nur eines einzigen Threads zur Verarbeitung mehrerer Kanäle besteht darin, dass weniger Threads zur Verarbeitung der Kanäle erforderlich sind. Tatsächlich ist es möglich, nur einen Thread zur Verarbeitung aller Kanäle zu verwenden. Für das Betriebssystem ist der Kontextwechsel zwischen Threads sehr kostspielig und jeder Thread belegt einige Systemressourcen (z. B. Speicher). Daher gilt: Je weniger Threads verwendet werden, desto besser.
Bedenken Sie jedoch, dass moderne Betriebssysteme und CPUs beim Multitasking immer besser werden, sodass der Overhead von Multithreading mit der Zeit immer kleiner wird. Wenn eine CPU über mehrere Kerne verfügt, kann der Verzicht auf Multitasking sogar eine Verschwendung von CPU-Leistung darstellen. Wie auch immer, die Diskussion dieses Designs sollte in einem anderen Artikel erfolgen. Hier reicht es zu wissen, dass Sie mit Selector mehrere Kanäle verwalten können.
Das Folgende ist ein Beispieldiagramm eines einzelnen Threads, der einen Selektor zum Verarbeiten von drei Kanälen verwendet:
(2)Erstellung eines Selektors
Erstellen Sie einen Selektor, indem Sie die Methode Selector.open() wie folgt aufrufen:
Kopieren Sie den Codecode wie folgt:
Selektor selector = Selector.open();
(3) Registrieren Sie den Kanal mit Selector
Um Kanal und Selektor zusammen zu verwenden, muss der Kanal beim Selektor registriert werden. Dies wird durch die Methode SelectableChannel.register() wie folgt erreicht:
Kopieren Sie den Codecode wie folgt:
channel.configureBlocking(false);
SelectionKey key =channel.register(selector,
Selectionkey.OP_READ);
Bei Verwendung mit einem Selektor muss sich der Kanal im nicht blockierenden Modus befinden. Das bedeutet, dass Sie FileChannel nicht mit einem Selector verwenden können, da FileChannel nicht in den nicht blockierenden Modus geschaltet werden kann. Socket-Kanäle sind in Ordnung.
Beachten Sie den zweiten Parameter der Methode register(). Dabei handelt es sich um eine „Interessensammlung“, also die Ereignisse, die Sie interessieren, wenn Sie den Kanal über den Selector anhören. Es gibt vier verschiedene Arten von Ereignissen, die abgehört werden können:
Verbinden
Akzeptieren
Lesen
Schreiben
Ein Kanal, der ein Ereignis auslöst, bedeutet, dass das Ereignis bereit ist. Daher wird ein Kanal, der erfolgreich eine Verbindung zu einem anderen Server herstellt, als „verbindungsbereit“ bezeichnet. Ein Server-Socket-Kanal gilt als „empfangsbereit“, wenn er bereit ist, eingehende Verbindungen zu empfangen. Ein Kanal, der Daten zum Lesen hat, wird als „lesebereit“ bezeichnet. Ein Kanal, der darauf wartet, Daten zu schreiben, kann als „schreibbereit“ bezeichnet werden.
Diese vier Ereignisse werden durch die vier Konstanten von SelectionKey dargestellt:
SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE
Wenn Sie an mehr als einem Ereignis interessiert sind, können Sie die Konstanten wie folgt mit dem bitweisen OR-Operator verbinden:
Kopieren Sie den Codecode wie folgt:
int InterestSet = SelectionKey.OP_READ |.
Interessensammlungen werden unten erwähnt.
(4)Auswahlschlüssel
Im vorherigen Abschnitt gibt die Methode register() bei der Registrierung eines Kanals beim Selector ein SelectionKey-Objekt zurück. Dieses Objekt enthält einige Eigenschaften, die für Sie von Interesse sein könnten:
Zinserhebung
fertige Sammlung
Kanal
Wähler
Zusätzliche Objekte (optional)
Nachfolgend beschreibe ich diese Eigenschaften.
Zinserhebung
Wie im Abschnitt „Registrieren eines Kanals mit einem Selektor“ beschrieben, handelt es sich bei der Interessensammlung um eine Sammlung interessanter Ereignisse, die Sie auswählen. Sie können die Interessensammlung über SelectionKey wie folgt lesen und schreiben:
Kopieren Sie den Codecode wie folgt:
int InterestSet = SelectionKey.Interess();
boolean isInterestedInAccept= (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = InterestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = InterestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = InterestSet & SelectionKey.OP_WRITE;
Es ist ersichtlich, dass Sie durch die Verwendung von „Bit AND“ zum Betreiben der Interessensammlung und der angegebenen SelectionKey-Konstante bestimmen können, ob sich ein bestimmtes Ereignis in der Interessensammlung befindet.
fertige Sammlung
Der Bereitschaftssatz ist der Satz von Vorgängen, für die der Kanal bereit ist. Nach einer Auswahl (Auswahl) gelangen Sie zunächst zum fertigen Set. Die Auswahl wird im nächsten Abschnitt erläutert. Auf die fertige Sammlung kann wie folgt zugegriffen werden:
int readySet = SelectionKey.readyOps();
Sie können dieselbe Methode wie die Erkennung der Interessensammlung verwenden, um zu erkennen, welche Ereignisse oder Vorgänge im Kanal bereit sind. Es stehen jedoch auch die folgenden vier Methoden zur Verfügung, die alle einen booleschen Typ zurückgeben:
Kopieren Sie den Codecode wie folgt:
SelectionKey.isAcceptable();
SelectionKey.isConnectable();
SelectionKey.isReadable();
SelectionKey.isWritable();
Kanal+Auswahl
Der Zugriff auf Channel und Selector über SelectionKey ist einfach. wie folgt:
Kopieren Sie den Codecode wie folgt:
Channelchannel=selectionKey.channel();
Selektor selector = SelectionKey.selector();
zusätzliche Objekte
An den SelectionKey können ein Objekt oder weitere Informationen angehängt werden, um einen bestimmten Kanal leicht zu identifizieren. Sie können beispielsweise einen Puffer zur Verwendung mit einem Kanal oder ein Objekt anhängen, das aggregierte Daten enthält. So verwenden Sie es:
Kopieren Sie den Codecode wie folgt:
SelectionKey.attach(theObject);
Object attachmentObj = SelectionKey.attachment();
Sie können auch Objekte anhängen, wenn Sie den Kanal mit dem Selector mithilfe der Methode register() registrieren. wie:
Kopieren Sie den Codecode wie folgt:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
(5) Wählen Sie den Kanal über den Selector aus
Sobald ein oder mehrere Kanäle bei einem Selector registriert sind, können mehrere überladene select()-Methoden aufgerufen werden. Diese Methoden geben die Kanäle zurück, die für das Ereignis bereit sind, an dem Sie interessiert sind (z. B. Verbinden, Akzeptieren, Lesen oder Schreiben). Mit anderen Worten: Wenn Sie an „lesebereiten“ Kanälen interessiert sind, gibt die Methode select() die Kanäle zurück, für die Leseereignisse bereit sind.
Hier ist die Methode select():
int select()
int select(langes Timeout)
int selectNow()
select() blockiert, bis mindestens ein Kanal für das von Ihnen registrierte Ereignis bereit ist.
select(long timeout) ist dasselbe wie select(), außer dass es für bis zu timeout Millisekunden (Parameter) blockiert.
selectNow() blockiert nicht und kehrt sofort zurück, unabhängig davon, welcher Kanal bereit ist (Anmerkung des Übersetzers: Diese Methode führt eine nicht blockierende Auswahloperation durch. Wenn seit der vorherigen Auswahloperation kein Kanal auswählbar geworden ist, gibt diese Methode direkt Null zurück.)
Der von der Methode select() zurückgegebene int-Wert gibt an, wie viele Kanäle bereit sind. Das heißt, wie viele Kanäle seit dem letzten Aufruf der Methode select() bereit geworden sind. Wenn die Methode select() aufgerufen wird, wird 1 zurückgegeben, da ein Kanal bereit wird. Wenn die Methode select() erneut aufgerufen wird und ein anderer Kanal bereit ist, wird sie erneut 1 zurückgegeben. Wenn auf dem ersten Bereitschaftskanal keine Operationen ausgeführt werden, gibt es jetzt zwei Bereitschaftskanäle, aber zwischen jedem Aufruf der select()-Methode ist nur ein Kanal bereit.
selectedKeys()
Sobald die Methode select() aufgerufen wird und der Rückgabewert angibt, dass ein oder mehrere Kanäle bereit sind, kann auf die bereiten Kanäle im „ausgewählten Schlüsselsatz“ durch Aufrufen der Methode selectedKeys() des Selektors zugegriffen werden. Wie unten gezeigt:
Kopieren Sie den Codecode wie folgt:
Set selectedKeys = selector.selectedKeys();
Beim Registrieren eines Kanals wie eines Selektors gibt die Methode Channel.register() ein SelectionKey-Objekt zurück. Dieses Objekt stellt den beim Selector registrierten Kanal dar. Auf diese Objekte kann über die selectedKeySet()-Methode von SelectionKey zugegriffen werden.
Auf bereite Kanäle kann durch Durchlaufen dieses ausgewählten Tastensatzes zugegriffen werden. wie folgt:
Kopieren Sie den Codecode wie folgt:
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// Eine Verbindung wurde von einem ServerSocketChannel akzeptiert.
} else if (key.isConnectable()) {
// Es wurde eine Verbindung mit einem Remote-Server hergestellt.
} else if (key.isReadable()) {
// Ein Kanal ist zum Lesen bereit
} else if (key.isWritable()) {
// Ein Kanal ist zum Schreiben bereit
}
keyIterator.<tuihighlight><a href="javascript:;" style="display:inline;float:none;position:inherit;cursor:pointer;color:#7962D5;text-decoration:underline;">remove</a ></tuihighlight>();
}
Diese Schleife durchläuft jeden Schlüssel im ausgewählten Schlüsselsatz und erkennt das Bereitschaftsereignis für den Kanal, der jedem Schlüssel entspricht.
Beachten Sie den Aufruf von keyIterator.remove() am Ende jeder Iteration. Der Selector entfernt keine SelectionKey-Instanzen aus dem ausgewählten Schlüsselsatz selbst. Muss bei der Bearbeitung des Kanals selbst entfernt werden. Wenn der Kanal das nächste Mal bereit ist, wird er vom Selector wieder in den ausgewählten Tastensatz verschoben.
Der von der Methode SelectionKey.channel() zurückgegebene Kanal muss in den Typ konvertiert werden, den Sie verarbeiten möchten, z. B. ServerSocketChannel oder SocketChannel usw.
(6)wakeUp()
Ein Thread wird nach dem Aufruf der Methode select() blockiert. Auch wenn kein Kanal bereit ist, gibt es eine Möglichkeit, ihn von der Methode select() zurückzugeben. Lassen Sie einfach andere Threads die Selector.wakeup()-Methode für das Objekt aufrufen, bei dem der erste Thread die select()-Methode aufgerufen hat. Der für die Methode select() blockierte Thread wird sofort zurückgegeben.
Wenn ein anderer Thread die Methode wakeup() aufruft, aber derzeit kein Thread für die Methode select() blockiert ist, wird der nächste Thread, der die Methode select() aufruft, sofort „aufwachen“.
(7)schließen()
Der Aufruf seiner Methode close() nach der Verwendung des Selectors schließt den Selector und macht alle beim Selector registrierten SelectionKey-Instanzen ungültig. Der Kanal selbst wird nicht geschlossen.
(8) Vollständiges Beispiel
Hier ist ein vollständiges Beispiel: Öffnen Sie einen Selektor, registrieren Sie einen Kanal beim Selektor (der Initialisierungsprozess des Kanals entfällt) und überwachen Sie dann kontinuierlich, ob die vier Ereignisse des Selektors (Akzeptieren, Verbinden, Lesen, Schreiben) bereit sind.
Kopieren Sie den Codecode wie folgt:
Selektor selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
int readyChannels = selector.select();
if(readyChannels == 0) continue;
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// Eine Verbindung wurde von einem ServerSocketChannel akzeptiert.
} else if (key.isConnectable()) {
// Es wurde eine Verbindung mit einem Remote-Server hergestellt.
} else if (key.isReadable()) {
// Ein Kanal ist zum Lesen bereit
} else if (key.isWritable()) {
// Ein Kanal ist zum Schreiben bereit
}
keyIterator.<tuihighlight><a href="javascript:;" style="display:inline;float:none;position:inherit;cursor:pointer;color:#7962D5;text-decoration:underline;">remove</a ></tuihighlight>();
}
}
Dateikanal
(Link zum Originaltext dieses Abschnitts, Autor: Jakob Jenkov, Übersetzer: Zhou Tai, Korrekturleser: Ding Yi)
FileChannel in Java NIO ist ein Kanal, der mit einer Datei verbunden ist. Dateien können über Dateikanäle gelesen und geschrieben werden.
FileChannel kann nicht auf den nicht blockierenden Modus eingestellt werden, es läuft immer im blockierenden Modus.
OpenFileChannel
Bevor FileChannel verwendet werden kann, muss es geöffnet werden. Wir können einen FileChannel jedoch nicht direkt öffnen. Wir müssen eine FileChannel-Instanz mithilfe eines InputStream, OutputStream oder RandomAccessFile erhalten. Hier ist ein Beispiel für das Öffnen eines FileChannel über RandomAccessFile:
Kopieren Sie den Codecode wie folgt:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
Daten aus FileChannel lesen
Rufen Sie eine der mehreren read()-Methoden auf, um Daten aus dem FileChannel zu lesen. wie:
Kopieren Sie den Codecode wie folgt:
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
Weisen Sie zunächst einen Puffer zu. Von FileChannel gelesene Daten werden in Buffer eingelesen.
Rufen Sie dann die Methode FileChannel.read() auf. Diese Methode liest Daten von FileChannel in Buffer. Der von der Methode read() zurückgegebene int-Wert gibt an, wie viele Bytes in den Puffer eingelesen wurden. Wenn -1 zurückgegeben wird, bedeutet dies, dass das Ende der Datei erreicht wurde.
Daten in FileChannel schreiben
Verwenden Sie die Methode FileChannel.write(), um Daten in FileChannel zu schreiben. Der Parameter dieser Methode ist ein Puffer. wie:
Kopieren Sie den Codecode wie folgt:
String newData = „Neuer String zum Schreiben in die Datei …“ + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
Kanal.write(buf);
}
Beachten Sie, dass FileChannel.write() in einer While-Schleife aufgerufen wird. Da es keine Garantie dafür gibt, wie viele Bytes die write()-Methode gleichzeitig in den FileChannel schreiben kann, muss die write()-Methode wiederholt aufgerufen werden, bis im Puffer keine Bytes mehr vorhanden sind, die nicht in den Kanal geschrieben wurden.
CloseFileChannel
Der FileChannel muss geschlossen werden, wenn Sie damit fertig sind. wie:
Kopieren Sie den Codecode wie folgt:
Kanal.close();
FileChannel-Positionsmethode
Manchmal kann es erforderlich sein, Daten an einer bestimmten Stelle im FileChannel zu lesen/schreiben. Sie können die aktuelle Position von FileChannel abrufen, indem Sie die Methode position() aufrufen.
Sie können die aktuelle Position von FileChannel auch festlegen, indem Sie die Methode position(long pos) aufrufen.
Hier zwei Beispiele:
Kopieren Sie den Codecode wie folgt:
long pos = channel.position();
Kanalposition(pos +123);
Wenn Sie die Position nach dem Ende der Datei festlegen und dann versuchen, Daten aus dem Dateikanal zu lesen, gibt die Lesemethode -1 zurück – das Flag für das Ende der Datei.
Wenn Sie die Position nach dem Ende der Datei festlegen und dann Daten in den Kanal schreiben, wird die Datei bis zur aktuellen Position erweitert und die Daten werden geschrieben. Dies kann zu „Dateilöchern“ führen, d. h. Lücken zwischen den in den physischen Dateien auf der Festplatte geschriebenen Daten.
FileChannel-Größenmethode
Die size()-Methode einer FileChannel-Instanz gibt die Größe der mit der Instanz verknüpften Datei zurück. wie:
Kopieren Sie den Codecode wie folgt:
long fileSize =channel.size();
Die Truncate-Methode von FileChannel
Sie können die Methode FileChannel.truncate() verwenden, um eine Datei abzufangen. Beim Abfangen einer Datei wird der Teil nach der angegebenen Länge der Datei gelöscht. wie:
Kopieren Sie den Codecode wie folgt:
Kanal.truncate(1024);
In diesem Beispiel werden die ersten 1024 Bytes der Datei abgefangen.
Force-Methode von FileChannel
Die Methode FileChannel.force() erzwingt Daten im Kanal, die noch nicht auf die Festplatte geschrieben wurden. Aus Leistungsgründen speichert das Betriebssystem Daten im Speicher zwischen, daher gibt es keine Garantie dafür, dass in FileChannel geschriebene Daten sofort auf die Festplatte geschrieben werden. Um dies sicherzustellen, muss die Methode force() aufgerufen werden.
Die Methode „force()“ verfügt über einen booleschen Parameter, der angibt, ob gleichzeitig Dateimetadaten (Berechtigungsinformationen usw.) auf die Festplatte geschrieben werden sollen.
Das folgende Beispiel erzwingt, dass sowohl Dateidaten als auch Metadaten auf die Festplatte übertragen werden:
Kopieren Sie den Codecode wie folgt:
channel.force(true);
Steckdosenkanal
(Link zum Originaltext dieses Abschnitts, Autor: Jakob Jenkov, Übersetzer: Zheng Yuting, Korrekturleser: Ding Yi)
SocketChannel in Java NIO ist ein Kanal, der mit einem TCP-Netzwerk-Socket verbunden ist. SocketChannel kann auf zwei Arten erstellt werden:
Öffnen Sie einen SocketChannel und stellen Sie eine Verbindung zu einem Server im Internet her.
Wenn eine neue Verbindung bei ServerSocketChannel eintrifft, wird ein SocketChannel erstellt.
Öffnen Sie SocketChannel
So öffnen Sie SocketChannel:
Kopieren Sie den Codecode wie folgt:
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
SocketChannel schließen
Wenn Sie mit dem SocketChannel fertig sind, rufen Sie SocketChannel.close() auf, um den SocketChannel zu schließen:
Kopieren Sie den Codecode wie folgt:
socketChannel.close();
Daten von SocketChannel lesen
Um Daten aus einem SocketChannel zu lesen, rufen Sie eine der read()-Methoden auf. Hier sind Beispiele:
Kopieren Sie den Codecode wie folgt:
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);
Weisen Sie zunächst einen Puffer zu. Die von SocketChannel gelesenen Daten werden in diesem Puffer abgelegt.
Rufen Sie dann SocketChannel.read() auf. Diese Methode liest Daten von SocketChannel in Buffer. Der von der Methode read() zurückgegebene int-Wert gibt an, wie viele Bytes in den Puffer eingelesen wurden. Wenn -1 zurückgegeben wird, bedeutet dies, dass das Ende des Streams gelesen wurde (die Verbindung wurde geschlossen).
Schreiben Sie an SocketChannel
Das Schreiben von Daten in SocketChannel verwendet die Methode SocketChannel.write(), die einen Puffer als Parameter akzeptiert. Beispiele sind wie folgt:
Kopieren Sie den Codecode wie folgt:
String newData = „Neuer String zum Schreiben in die Datei …“ + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
Kanal.write(buf);
}
Beachten Sie, dass die Methode SocketChannel.write() in einer While-Schleife aufgerufen wird. Die Methode Write() kann nicht garantieren, wie viele Bytes in den SocketChannel geschrieben werden können. Also rufen wir write() wiederholt auf, bis der Puffer keine Bytes mehr zum Schreiben hat.
nicht blockierender Modus
Sie können SocketChannel in den nicht blockierenden Modus versetzen. Nach der Einstellung können Sie connect(), read() und write() im asynchronen Modus aufrufen.
verbinden()
Wenn sich SocketChannel im nicht blockierenden Modus befindet und connect() zu diesem Zeitpunkt aufgerufen wird, kehrt die Methode möglicherweise zurück, bevor die Verbindung hergestellt wird. Um festzustellen, ob die Verbindung hergestellt wurde, können Sie die Methode finishConnect() aufrufen. So was:
Kopieren Sie den Codecode wie folgt:
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
while(! socketChannel.finishConnect() ){
//warte, oder mach etwas anderes...
}
schreiben()
Im nicht blockierenden Modus kehrt die Methode write() möglicherweise zurück, bevor etwas geschrieben wird. Daher muss write() in der Schleife aufgerufen werden. Es gab bereits Beispiele, daher werde ich hier nicht näher darauf eingehen.
lesen()
Im nicht blockierenden Modus kehrt die Methode read() möglicherweise zurück, bevor Daten gelesen wurden. Sie müssen also auf den Rückgabewert int achten, der Ihnen sagt, wie viele Bytes gelesen wurden.
Nicht blockierender Modus und Selektoren
Der nicht blockierende Modus funktioniert besser mit Selektoren. Durch die Registrierung eines oder mehrerer SocketChannels beim Selektor können Sie den Selektor fragen, welcher Kanal zum Lesen, Schreiben usw. bereit ist. Die Kombination von Selector und SocketChannel wird später ausführlich besprochen.
ServerSocket-Kanal
(Link zum Originaltext dieses Abschnitts, Autor: Jakob Jenkov, Übersetzer: Zheng Yuting, Korrekturleser: Ding Yi)
ServerSocketChannel in Java NIO ist ein Kanal, der auf neue eingehende TCP-Verbindungen hören kann, genau wie ServerSocket in Standard-IO. Die ServerSocketChannel-Klasse befindet sich im Paket java.nio.channels.
Hier ist ein Beispiel:
Kopieren Sie den Codecode wie folgt:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
while(true){
SocketChannel socketChannel =
serverSocketChannel.accept();
//etwas mit socketChannel machen...
}
Öffnen Sie ServerSocketChannel
Öffnen Sie den ServerSocketChannel, indem Sie die Methode ServerSocketChannel.open() aufrufen. Beispiel:
Kopieren Sie den Codecode wie folgt:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
Schließen Sie ServerSocketChannel
Schließen Sie ServerSocketChannel, indem Sie die Methode ServerSocketChannel.close() aufrufen.
Kopieren Sie den Codecode wie folgt:
serverSocketChannel.close();
Achten Sie auf neue eingehende Verbindungen
Lauschen Sie über die Methode ServerSocketChannel.accept() auf neue eingehende Verbindungen. Wenn die Methode „accept()“ zurückkehrt, gibt sie einen SocketChannel zurück, der die neue eingehende Verbindung enthält. Daher blockiert die Methode „accept()“, bis eine neue Verbindung eintrifft.
Anstatt nur eine Verbindung abzuhören, wird die Methode „accept()“ normalerweise in der while-Schleife aufgerufen, wie im folgenden Beispiel:
Kopieren Sie den Codecode wie folgt:
while(true){
SocketChannel socketChannel =
serverSocketChannel.accept();
//etwas mit socketChannel machen...
}
Natürlich können Sie in der while-Schleife neben true auch andere Exit-Kriterien verwenden.
nicht blockierender Modus
ServerSocketChannel kann auf den nicht blockierenden Modus eingestellt werden. Im nicht blockierenden Modus kehrt die Methode „accept()“ sofort zurück. Wenn keine neue eingehende Verbindung vorliegt, ist der Rückgabewert null. Daher müssen Sie prüfen, ob der zurückgegebene SocketChannel null ist. wie:
Kopieren Sie den Codecode wie folgt:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);
while(true){
SocketChannel socketChannel =
serverSocketChannel.accept();
if(socketChannel != null){
//etwas mit socketChannel machen...
}
}
Datagrammkanal
(Link zum Originaltext dieses Abschnitts, Autor: Jakob Jenkov, Übersetzer: Zheng Yuting, Korrekturleser: Ding Yi)
DatagramChannel in Java NIO ist ein Kanal, der UDP-Pakete senden und empfangen kann. Da UDP ein verbindungsloses Netzwerkprotokoll ist, kann es nicht wie andere Kanäle gelesen und geschrieben werden. Es sendet und empfängt Datenpakete.
OpenDatagramChannel
So wird DatagramChannel geöffnet:
Kopieren Sie den Codecode wie folgt:
DatagramChannel-Kanal = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9999));
Der in diesem Beispiel geöffnete DatagramChannel kann Pakete auf dem UDP-Port 9999 empfangen.
Daten empfangen
Empfangen Sie Daten von DatagramChannel über die Methode „receive()“, z. B.:
Kopieren Sie den Codecode wie folgt:
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);
Die Methode „receive()“ kopiert den Inhalt des empfangenen Datenpakets in den angegebenen Puffer. Wenn der Puffer die empfangenen Daten nicht aufnehmen kann, werden die überschüssigen Daten verworfen.
Daten senden
Senden Sie Daten von DatagramChannel über die send()-Methode, wie zum Beispiel:
Kopieren Sie den Codecode wie folgt:
String newData = „Neuer String zum Schreiben in die Datei …“ + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
int bytesSent =channel.send(buf, new InetSocketAddress("jenkov.com", 80));
In diesem Beispiel wird eine Zeichenfolge an den UDP-Port 80 des Servers „jenkov.com“ gesendet. Da der Server diesen Port nicht überwacht, passiert nichts. Es wird Ihnen auch nicht mitgeteilt, ob das ausgehende Paket empfangen wurde, da UDP keine Garantien hinsichtlich der Datenzustellung hat.
Stellen Sie eine Verbindung zu einer bestimmten Adresse her
Ein DatagramChannel kann mit einer bestimmten Adresse im Netzwerk „verbunden“ werden. Da UDP verbindungslos ist, wird durch die Verbindung zu einer bestimmten Adresse keine echte Verbindung wie bei einem TCP-Kanal hergestellt. Stattdessen ist der DatagramChannel gesperrt, sodass er nur Daten von einer bestimmten Adresse senden und empfangen kann.
Hier ist ein Beispiel:
Kopieren Sie den Codecode wie folgt:
channel.connect(new InetSocketAddress("jenkov.com", 80));
Sobald die Verbindung hergestellt ist, können Sie auch die Methoden read() und write() wie bei einem herkömmlichen Kanal verwenden. Es gibt lediglich keine Garantien hinsichtlich der Datenübertragung. Hier ein paar Beispiele:
Kopieren Sie den Codecode wie folgt:
int bytesRead = channel.read(buf);
int bytesWritten =channel.write(but);
Rohr
(Link zum Originaltext dieses Abschnitts, Autor: Jakob Jenkov, Übersetzer: Huang Zhong, Korrekturleser: Ding Yi)
Eine Java NIO-Pipe ist eine unidirektionale Datenverbindung zwischen zwei Threads. Das Rohr hat einen Quellkanal und einen Senkenkanal. Daten werden in den Senkenkanal geschrieben und vom Quellkanal gelesen.
Hier ist eine Veranschaulichung des Pipe-Prinzips:
Pipeline erstellen
Öffnen Sie die Pipe über die Methode Pipe.open(). Zum Beispiel:
Kopieren Sie den Codecode wie folgt:
Pipe pipe = Pipe.open();
Daten in die Pipe schreiben
Um Daten in die Pipe zu schreiben, müssen Sie auf den Senkenkanal zugreifen. So was:
Kopieren Sie den Codecode wie folgt:
Pipe.SinkChannel sinkChannel = pipe.sink();
Schreiben Sie Daten in SinkChannel, indem Sie die Methode write() von SinkChannel aufrufen, etwa so:
Kopieren Sie den Codecode wie folgt:
String newData = „Neuer String zum Schreiben in die Datei …“ + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
<b>sinkChannel.write(buf);</b>
}
[Code]
Daten aus der Pipe lesen
Um Daten aus einer Pipe zu lesen, müssen Sie wie folgt auf den Quellkanal zugreifen:
[Code]
Pipe.SourceChannel sourceChannel = pipe.source();
Rufen Sie die read()-Methode des Quellkanals auf, um die Daten wie folgt zu lesen:
Kopieren Sie den Codecode wie folgt:
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
Der von der Methode read() zurückgegebene int-Wert sagt uns, wie viele Bytes in den Puffer gelesen wurden.