Einführung in den Java NIO Selector

1. Übersicht

In diesem Artikel werden die einführenden Teile der Selector- Komponente von Java NIO erläutert .

Ein Selektor bietet einen Mechanismus zum Überwachen eines oder mehrerer NIO-Kanäle und zum Erkennen, wann einer oder mehrere für die Datenübertragung verfügbar werden.

Auf diese Weise kann ein einzelner Thread zum Verwalten mehrerer Kanäle und damit mehrerer Netzwerkverbindungen verwendet werden.

2. Warum einen Selektor verwenden?

Mit einem Selektor können wir einen Thread anstelle von mehreren verwenden, um mehrere Kanäle zu verwalten. Das Umschalten des Kontexts zwischen Threads ist für das Betriebssystem teuer , und außerdem belegt jeder Thread Speicherplatz.

Je weniger Threads wir verwenden, desto besser. Es ist jedoch wichtig zu bedenken, dass moderne Betriebssysteme und CPUs beim Multitasking immer besser werden , sodass der Aufwand für Multithreading mit der Zeit immer geringer wird.

Wir werden uns hier damit befassen, wie wir mit einem Selektor mehrere Kanäle mit einem einzigen Thread behandeln können.

Beachten Sie auch, dass Selektoren Ihnen nicht nur beim Lesen von Daten helfen. Sie können auch auf eingehende Netzwerkverbindungen warten und Daten über langsame Kanäle schreiben.

3. Setup

Um den Selektor zu verwenden, benötigen wir keine spezielle Einrichtung. Alle Klassen, die wir brauchen, sind das Kernpaket java.nio und wir müssen nur das importieren, was wir brauchen.

Danach können wir mehrere Kanäle mit einem Selektorobjekt registrieren. Wenn E / A-Aktivitäten auf einem der Kanäle stattfinden, benachrichtigt uns der Selektor. Auf diese Weise können wir aus einer großen Anzahl von Datenquellen aus einem einzelnen Thread lesen.

Jeder Kanal, den wir bei einem Selektor registrieren, muss eine Unterklasse von SelectableChannel sein . Dies ist eine spezielle Art von Kanälen, die in den nicht blockierenden Modus versetzt werden können.

4. Erstellen eines Selektors

Ein Wähler kann durch den Aufruf der statischen erstellt werden offene Methode der Selector - Klasse, die das System des Standard - Selektor - Provider verwenden einen neuen Selektor zu erstellen:

Selector selector = Selector.open();

5. Registrieren auswählbarer Kanäle

Damit ein Selektor Kanäle überwachen kann, müssen wir diese Kanäle beim Selektor registrieren. Dazu rufen wir die Registermethode des auswählbaren Kanals auf.

Bevor ein Kanal bei einem Selektor registriert wird, muss er sich im nicht blockierenden Modus befinden:

channel.configureBlocking(false); SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

Dies bedeutet, dass wir FileChannel s nicht mit einem Selektor verwenden können, da sie nicht wie bei Socket-Kanälen in den nicht blockierenden Modus geschaltet werden können.

Der erste Parameter ist das zuvor erstellte Selector- Objekt. Der zweite Parameter definiert einen Interessensatz , dh welche Ereignisse wir über den Selector im überwachten Kanal abhören möchten.

Wir können auf vier verschiedene Ereignisse hören, die jeweils durch eine Konstante in der SelectionKey- Klasse dargestellt werden:

  • Verbinden - Wenn ein Client versucht, eine Verbindung zum Server herzustellen. Dargestellt durch SelectionKey.OP_CONNECT
  • Akzeptieren - Wenn der Server eine Verbindung von einem Client akzeptiert. Vertreten durch SelectionKey.OP_ACCEPT
  • Lesen - wenn der Server bereit ist, vom Kanal zu lesen. Vertreten durch SelectionKey.OP_READ
  • Schreiben - wenn der Server bereit ist, in den Kanal zu schreiben. Vertreten durch SelectionKey.OP_WRITE

Das zurückgegebene Objekt SelectionKey repräsentiert die Registrierung des auswählbaren Kanals beim Selektor. Wir werden es im folgenden Abschnitt weiter betrachten.

6. Das SelectionKey- Objekt

Wie wir im vorherigen Abschnitt gesehen haben, erhalten wir beim Registrieren eines Kanals mit einem Selektor ein SelectionKey- Objekt. Dieses Objekt enthält Daten, die die Registrierung des Kanals darstellen.

Es enthält einige wichtige Eigenschaften, die wir gut verstehen müssen, um den Selektor auf dem Kanal verwenden zu können. Wir werden uns diese Eigenschaften in den folgenden Unterabschnitten ansehen.

6.1. Das Interessenset

Ein Interessensatz definiert den Satz von Ereignissen, auf die der Selektor auf diesem Kanal achten soll. Es ist ein ganzzahliger Wert. Wir können diese Informationen auf folgende Weise erhalten.

Erstens haben wir das Interesse Satz von der zurück SelectionKey ‚s interestOps Methode. Dann haben wir die Ereigniskonstante in SelectionKey, die wir uns zuvor angesehen haben.

Wenn wir UND diese beiden Werte UND, erhalten wir einen booleschen Wert, der uns sagt, ob das Ereignis überwacht wird oder nicht:

int interestSet = selectionKey.interestOps(); boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT; boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT; boolean isInterestedInRead = interestSet & SelectionKey.OP_READ; boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;

6.2. Das fertige Set

Der Ready-Satz definiert den Satz von Ereignissen, für die der Kanal bereit ist. Es ist auch ein ganzzahliger Wert; Wir können diese Informationen auf folgende Weise erhalten.

Wir haben die bereit zurückgegeben bekam von SelectionKey ‚s readyOps Methode. Wenn wir diesen Wert mit den Ereigniskonstanten UND verknüpfen, wie wir es im Fall von Interesse getan haben, erhalten wir einen Booleschen Wert, der angibt, ob der Kanal für einen bestimmten Wert bereit ist oder nicht.

Eine andere alternative und kürzere Möglichkeit, dies zu tun, besteht darin, die Convenience-Methoden von SelectionKey für denselben Zweck zu verwenden:

selectionKey.isAcceptable(); selectionKey.isConnectable(); selectionKey.isReadable(); selectionKey.isWriteable();

6.3. Der Kanal

Der Zugriff auf den Kanal, der über das SelectionKey- Objekt überwacht wird, ist sehr einfach. Wir nennen einfach die Kanalmethode :

Channel channel = key.channel();

6.4. Der Selektor

Genau wie beim Abrufen eines Kanals ist es sehr einfach, das Selector- Objekt vom SelectionKey- Objekt abzurufen :

Selector selector = key.selector();

6.5. Objekte anhängen

Wir können ein Objekt an einen SelectionKey anhängen . Manchmal möchten wir einem Kanal eine benutzerdefinierte ID geben oder ein Java-Objekt anhängen, das wir möglicherweise verfolgen möchten.

Das Anbringen von Objekten ist eine praktische Möglichkeit. So hängen Sie Objekte an einen SelectionKey an und rufen sie ab :

key.attach(Object); Object object = key.attachment();

Alternativ können wir ein Objekt während der Kanalregistrierung anhängen. Wir fügen es als dritten Parameter zur Registermethode des Kanals hinzu , wie folgt:

SelectionKey key = channel.register( selector, SelectionKey.OP_ACCEPT, object);

7. Auswahl der Kanaltasten

Bisher haben wir uns angesehen, wie Sie einen Selektor erstellen, Kanäle registrieren und die Eigenschaften des SelectionKey- Objekts untersuchen, das die Registrierung eines Kanals für einen Selektor darstellt.

Dies ist nur die Hälfte des Prozesses. Jetzt müssen wir einen kontinuierlichen Prozess zur Auswahl des fertigen Satzes durchführen, den wir uns zuvor angesehen haben. Wir wählen mit der Auswahlmethode des Selektors aus , wie folgt:

int channels = selector.select();

This method blocks until at least one channel is ready for an operation. The integer returned represents the number of keys whose channels are ready for an operation.

Next, we usually retrieve the set of selected keys for processing:

Set selectedKeys = selector.selectedKeys();

The set we have obtained is of SelectionKey objects, each key represents a registered channel which is ready for an operation.

After this, we usually iterate over this set and for each key, we obtain the channel and perform any of the operations that appear in our interest set on it.

During the lifetime of a channel, it may be selected several times as its key appears in the ready set for different events. This is why we must have a continuous loop to capture and process channel events as and when they occur.

8. Complete Example

To cement the knowledge we have gained in the previous sections, we're going to build a complete client-server example.

For ease of testing out our code, we'll build an echo server and an echo client. In this kind of setup, the client connects to the server and starts sending messages to it. The server echoes back messages sent by each client.

When the server encounters a specific message, such as end, it interprets it as the end of the communication and closes the connection with the client.

8.1. The Server

Here is our code for EchoServer.java:

public class EchoServer { private static final String POISON_PILL = "POISON_PILL"; public static void main(String[] args) throws IOException { Selector selector = Selector.open(); ServerSocketChannel serverSocket = ServerSocketChannel.open(); serverSocket.bind(new InetSocketAddress("localhost", 5454)); serverSocket.configureBlocking(false); serverSocket.register(selector, SelectionKey.OP_ACCEPT); ByteBuffer buffer = ByteBuffer.allocate(256); while (true) { selector.select(); Set selectedKeys = selector.selectedKeys(); Iterator iter = selectedKeys.iterator(); while (iter.hasNext()) { SelectionKey key = iter.next(); if (key.isAcceptable()) { register(selector, serverSocket); } if (key.isReadable()) { answerWithEcho(buffer, key); } iter.remove(); } } } private static void answerWithEcho(ByteBuffer buffer, SelectionKey key) throws IOException { SocketChannel client = (SocketChannel) key.channel(); client.read(buffer); if (new String(buffer.array()).trim().equals(POISON_PILL)) { client.close(); System.out.println("Not accepting client messages anymore"); } else { buffer.flip(); client.write(buffer); buffer.clear(); } } private static void register(Selector selector, ServerSocketChannel serverSocket) throws IOException { SocketChannel client = serverSocket.accept(); client.configureBlocking(false); client.register(selector, SelectionKey.OP_READ); } public static Process start() throws IOException, InterruptedException { String javaHome = System.getProperty("java.home"); String javaBin = javaHome + File.separator + "bin" + File.separator + "java"; String classpath = System.getProperty("java.class.path"); String className = EchoServer.class.getCanonicalName(); ProcessBuilder builder = new ProcessBuilder(javaBin, "-cp", classpath, className); return builder.start(); } }

This is what is happening; we create a Selector object by calling the static open method. We then create a channel also by calling its static open method, specifically a ServerSocketChannel instance.

This is because ServerSocketChannel is selectable and good for a stream-oriented listening socket.

We then bind it to a port of our choice. Remember we said earlier that before registering a selectable channel to a selector, we must first set it to non-blocking mode. So next we do this and then register the channel to the selector.

We don't need the SelectionKey instance of this channel at this stage, so we will not remember it.

Java NIO uses a buffer-oriented model other than a stream-oriented model. So socket communication usually takes place by writing to and reading from a buffer.

We, therefore, create a new ByteBuffer which the server will be writing to and reading from. We initialize it to 256 bytes, it's just an arbitrary value, depending on how much data we plan to transfer to and fro.

Finally, we perform the selection process. We select the ready channels, retrieve their selection keys, iterate over the keys and perform the operations for which each channel is ready.

We do this in an infinite loop since servers usually need to keep running whether there is an activity or not.

The only operation a ServerSocketChannel can handle is an ACCEPT operation. When we accept the connection from a client, we obtain a SocketChannel object on which we can do read and writes. We set it to non-blocking mode and register it for a READ operation to the selector.

During one of the subsequent selections, this new channel will become read-ready. We retrieve it and read it contents into the buffer. True to it's as an echo server, we must write this content back to the client.

When we desire to write to a buffer from which we have been reading, we must call the flip() method.

We finally set the buffer to write mode by calling the flip method and simply write to it.

The start() method is defined so that the echo server can be started as a separate process during unit testing.

8.2. The Client

Here is our code for EchoClient.java:

public class EchoClient { private static SocketChannel client; private static ByteBuffer buffer; private static EchoClient instance; public static EchoClient start() { if (instance == null) instance = new EchoClient(); return instance; } public static void stop() throws IOException { client.close(); buffer = null; } private EchoClient() { try { client = SocketChannel.open(new InetSocketAddress("localhost", 5454)); buffer = ByteBuffer.allocate(256); } catch (IOException e) { e.printStackTrace(); } } public String sendMessage(String msg) { buffer = ByteBuffer.wrap(msg.getBytes()); String response = null; try { client.write(buffer); buffer.clear(); client.read(buffer); response = new String(buffer.array()).trim(); System.out.println("response=" + response); buffer.clear(); } catch (IOException e) { e.printStackTrace(); } return response; } }

The client is simpler than the server.

We use a singleton pattern to instantiate it inside the start static method. We call the private constructor from this method.

In the private constructor, we open a connection on the same port on which the server channel was bound and still on the same host.

We then create a buffer to which we can write and from which we can read.

Finally, we have a sendMessage method which reads wraps any string we pass to it into a byte buffer which is transmitted over the channel to the server.

Wir lesen dann vom Client-Kanal, um die vom Server gesendete Nachricht zu erhalten. Wir geben dies als Echo unserer Botschaft zurück.

8.3. Testen

In einer Klasse namens EchoTest.java erstellen wir einen Testfall, der den Server startet, Nachrichten an den Server sendet und nur dann erfolgreich ist, wenn dieselben Nachrichten vom Server zurück empfangen werden. Als letzter Schritt stoppt der Testfall den Server vor Abschluss.

Wir können jetzt den Test ausführen:

public class EchoTest { Process server; EchoClient client; @Before public void setup() throws IOException, InterruptedException { server = EchoServer.start(); client = EchoClient.start(); } @Test public void givenServerClient_whenServerEchosMessage_thenCorrect() { String resp1 = client.sendMessage("hello"); String resp2 = client.sendMessage("world"); assertEquals("hello", resp1); assertEquals("world", resp2); } @After public void teardown() throws IOException { server.destroy(); EchoClient.stop(); } }

9. Fazit

In diesem Artikel haben wir die grundlegende Verwendung der Java NIO Selector-Komponente behandelt.

Der vollständige Quellcode und alle Codefragmente für diesen Artikel sind in meinem GitHub-Projekt verfügbar.