Einführung in den Java NIO Selector

Einführung in den Java NIO Selector

1. Überblick

In diesem Artikel werden die einführenden Teile derSelector-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 Weisea single thread can be used for managing multiple channels und damit mehrere Netzwerkverbindungen.

2. Warum einen Selektor verwenden?

Mit einem Selektor können wir einen Thread anstelle mehrerer verwenden, um mehrere Kanäle zu verwalten. Context-switching between threads is expensive for the operating system und zusätzlicheach thread takes up memory.

Je weniger Threads wir verwenden, desto besser. Es ist jedoch wichtig, sich anmodern operating systems and CPU’s keep getting better at multitasking zu erinnern, damit der Overhead von 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. Konfiguration

Für die Verwendung des Selektors ist keine spezielle Einrichtung erforderlich. Alle Klassen, die wir brauchen, sind das Kernpaketjava.niound wir müssen nur das importieren, was wir brauchen.

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

Jeder Kanal, den wir bei einem Selektor registrieren, muss eine Unterklasse vonSelectableChannel sein. Hierbei handelt es sich um eine spezielle Art von Kanälen, die in den nicht blockierenden Modus versetzt werden können.

4. Selektor erstellen

Ein Selektor kann durch Aufrufen der statischenopen-Methode derSelector-Klasse erstellt werden, die den Standard-Selektoranbieter des Systems verwendet, um einen neuen Selektor zu erstellen:

Selector selector = Selector.open();

5. Registrierbare Kanäle registrieren

Damit ein Selektor Kanäle überwachen kann, müssen wir diese Kanäle beim Selektor registrieren. Dazu rufen wir dieregister-Methode 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 wirFileChannels 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 erstellteSelector-Objekt, der zweite Parameter definiert einen Interessensatz,, der über den Selektor angibt, auf welche Ereignisse wir im überwachten Kanal warten möchten.

Es gibt vier verschiedene Ereignisse, auf die wir hören können. Jedes wird durch eine Konstante in der KlasseSelectionKeydargestellt:

  • Connect, wenn ein Client versucht, eine Verbindung zum Server herzustellen. Dargestellt durchSelectionKey.OP_CONNECT

  • Accept, wenn der Server eine Verbindung von einem Client akzeptiert. Dargestellt durchSelectionKey.OP_ACCEPT

  • Read, wenn der Server bereit ist, vom Kanal zu lesen. Dargestellt durchSelectionKey.OP_READ

  • Write, wenn der Server bereit ist, in den Kanal zu schreiben. Dargestellt durchSelectionKey.OP_WRITE

Das zurückgegebene ObjektSelectionKey repräsentiert die Registrierung des auswählbaren Kanals beim Selektor. Wir werden uns das im folgenden Abschnitt genauer ansehen.

6. DasSelectionKey-Objekt

Wie wir im vorherigen Abschnitt gesehen haben, erhalten wir beim Registrieren eines Kanals mit einem Selektor einSelectionKey-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 Interest Set definiert die Menge der Ereignisse, 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 den Zinssatz nach der MethodeSelectionKeyinterestOpszurückgegeben. Dann haben wir die Ereigniskonstante inSelectionKey, die wir zuvor betrachtet haben.

Wenn wir UND diese beiden Werte verwenden, 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

Das Ready-Set definiert die Ereignismenge, für die der Kanal bereit ist. Es ist auch ein ganzzahliger Wert. Wir können diese Informationen auf folgende Weise erhalten.

Wir haben den fertigen Satz nach derreadyOps-Methode vonSelectionKeyzurückgegeben. 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,SelectionKey's Convenience-Methoden für denselben Zweck zu verwenden:

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

6.3. Der Kanal

Der Zugriff auf den Kanal, der vomSelectionKey-Objekt aus überwacht wird, ist sehr einfach. Wir rufen einfach die Methodechannelauf:

Channel channel = key.channel();

6.4. Der Selektor

Genau wie beim Abrufen eines Kanals ist es sehr einfach, dasSelector-Objekt aus demSelectionKey-Objekt abzurufen:

Selector selector = key.selector();

6.5. Objekte anhängen

Wir können ein Objekt anSelectionKey. anhängen. Manchmal möchten wir einem Kanal eine benutzerdefinierte ID zuweisen oder eine beliebige Art von Java-Objekt anhängen, das wir möglicherweise verfolgen möchten.

Das Anhängen von Objekten ist eine praktische Möglichkeit. So hängen Sie Objekte an und erhalten sie vonSelectionKey:

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 zurregister-Methode des Kanals hinzu:

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

7. Kanalschlüsselauswahl

Bisher haben wir uns angesehen, wie Sie einen Selektor erstellen, Kanäle registrieren und die Eigenschaften desSelectionKey-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 Auswahlprozess für das fertige Set durchführen, das wir uns zuvor angesehen haben. Wir führen die Auswahl mit derselect-Methode des Selektors durch:

int channels = selector.select();

Diese Methode blockiert, bis mindestens ein Kanal für eine Operation bereit ist. Die zurückgegebene Ganzzahl gibt die Anzahl der Tasten an, deren Kanäle für eine Operation bereit sind.

Als Nächstes rufen wir normalerweise den Satz ausgewählter Schlüssel zur Verarbeitung ab:

Set selectedKeys = selector.selectedKeys();

Der Satz, den wir erhalten haben, besteht ausSelectionKey Objekten. Jeder Schlüssel repräsentiert einen registrierten Kanal, der für eine Operation bereit ist.

Danach iterieren wir normalerweise über diesen Satz und erhalten für jeden Schlüssel den Kanal und führen alle Operationen aus, die in unserem Interesse für diesen Satz erscheinen.

Während der Laufzeit eines Kanals kann dieser mehrmals ausgewählt werden, da seine Taste im Ready-Set für verschiedene Ereignisse angezeigt wird. Aus diesem Grund müssen wir eine kontinuierliche Schleife haben, um Kanalereignisse zu erfassen und zu verarbeiten, sobald sie auftreten.

8. Komplettes Beispiel

Um das in den vorherigen Abschnitten erworbene Wissen zu festigen, werden wir ein vollständiges Client-Server-Beispiel erstellen.

Um das Testen unseres Codes zu vereinfachen, erstellen wir einen Echo-Server und einen Echo-Client. Bei dieser Art der Einrichtung stellt der Client eine Verbindung zum Server her und beginnt, Nachrichten an ihn zu senden. Der Server gibt die von jedem Client gesendeten Nachrichten zurück.

Wenn der Server auf eine bestimmte Nachricht wieend stößt, interpretiert er diese als das Ende der Kommunikation und schließt die Verbindung mit dem Client.

8.1. Der Kellner

Hier ist unser Code fürEchoServer.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");
        }

        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();
    }
}

Dies ist, was passiert; Wir erstellen einSelector-Objekt, indem wir die statischeopen-Methode aufrufen. Wir erstellen dann einen Kanal auch durch Aufrufen seiner statischenopen-Methode, insbesondere einerServerSocketChannel-Instanz.

Dies liegt daran, dassServerSocketChannel is selectable and good for a stream-oriented listening socket.

Wir binden es dann an einen Hafen unserer Wahl. Denken Sie daran, dass wir vor dem Registrieren eines auswählbaren Kanals für einen Selektor diesen zunächst in den nicht blockierenden Modus versetzen müssen. Also machen wir das als nächstes und registrieren dann den Kanal im Selektor.

Wir benötigen zu diesem Zeitpunkt nicht dieSelectionKey-Instanz dieses Kanals, daher werden wir uns nicht daran erinnern.

Java NIO verwendet ein anderes pufferorientiertes Modell als ein streamorientiertes Modell. Daher erfolgt die Socket-Kommunikation normalerweise durch Schreiben in einen Puffer und Lesen aus einem Puffer.

Wir erstellen daher ein neuesByteBuffer, in das der Server schreibt und von dem er liest. Wir initialisieren es auf 256 Bytes. Dies ist nur ein beliebiger Wert, je nachdem, wie viele Daten wir hin und her übertragen möchten.

Schließlich führen wir den Auswahlprozess durch. Wir wählen die bereiten Kanäle aus, rufen ihre Auswahltasten ab, iterieren über die Tasten und führen die Operationen aus, für die jeder Kanal bereit ist.

Wir tun dies in einer Endlosschleife, da Server normalerweise weiterlaufen müssen, unabhängig davon, ob eine Aktivität vorliegt oder nicht.

Die einzige Operation, die einServerSocketChannelausführen kann, ist eineACCEPT-Operation. Wenn wir die Verbindung von einem Client akzeptieren, erhalten wir einSocketChannel-Objekt, auf dem wir lesen und schreiben können. Wir versetzen es in den nicht blockierenden Modus und registrieren es für eine READ-Operation im Selektor.

Während einer der folgenden Auswahlen wird dieser neue Kanal lesebereit. Wir holen es ab und lesen es in den Puffer. Getreu als Echoserver müssen wir diesen Inhalt zurück auf den Client schreiben.

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

Wir setzen den Puffer schließlich auf den Schreibmodus, indem wir die Methodeflipaufrufen und einfach darauf schreiben.

Die Methodestart() ist so definiert, dass der Echoserver während des Komponententests als separater Prozess gestartet werden kann.

8.2. Der Kunde

Hier ist unser Code fürEchoClient.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;

    }
}

Der Client ist einfacher als der Server.

Wir verwenden ein Singleton-Muster, um es innerhalb der statischen Methode vonstartzu instanziieren. Wir rufen den privaten Konstruktor von dieser Methode auf.

Im privaten Konstruktor öffnen wir eine Verbindung auf demselben Port, an den der Serverkanal gebunden war, und auf demselben Host.

Wir erstellen dann einen Puffer, in den wir schreiben und aus dem wir lesen können.

Schließlich haben wir einesendMessage-Methode, die jede Zeichenfolge, die wir an sie übergeben, in einen Bytepuffer umschließt, der über den Kanal an den Server übertragen wird.

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

Innerhalb einer Klasse namensEchoTest.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. Im letzten Schritt stoppt der Testfall den Server vor dem 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 meinenGitHub project verfügbar.