Ein Handbuch zu NIO2 Asynchronous Socket Channel

Eine Anleitung zum asynchronen NIO2-Socket-Kanal

1. Überblick

In diesem Artikel wird gezeigt, wie ein einfacher Server und sein Client mithilfe der Java 7 NIO.2-Kanal-APIs erstellt werden.

Wir werden uns die KlassenAsynchronousServerSocketChannel undAsynchronousSocketChannel ansehen, die die Schlüsselklassen sind, die bei der Implementierung des Servers bzw. Clients verwendet werden.

Wenn Sie mit NIO.2-Kanal-APIs noch nicht vertraut sind, finden Sie auf dieser Site einen einführenden Artikel. Sie können es lesen, indem Sie diesenlink folgen.

Alle Klassen, die zur Verwendung von NIO.2-Kanal-APIs benötigt werden, sind injava.nio.channels Paket zusammengefasst:

import java.nio.channels.*;

2. Der Server mitFuture

Eine Instanz vonAsynchronousServerSocketChannel wird durch Aufrufen der statischen offenen API für ihre Klasse erstellt:

AsynchronousServerSocketChannel server
  = AsynchronousServerSocketChannel.open();

Ein neu erstellter asynchroner Server-Socket-Kanal ist offen, aber noch nicht gebunden. Daher müssen wir ihn an eine lokale Adresse binden und optional einen Port auswählen:

server.bind(new InetSocketAddress("127.0.0.1", 4555));

Wir hätten genauso gut null übergeben können, damit es eine lokale Adresse verwendet und an einen beliebigen Port bindet:

server.bind(null);

Nach dem Binden wird dieaccept-API verwendet, um das Akzeptieren von Verbindungen zum Socket des Kanals zu initiieren:

Future acceptFuture = server.accept();

Wie bei asynchronen Kanaloperationen wird der obige Aufruf sofort zurückgegeben und die Ausführung fortgesetzt.

Als Nächstes können wir dieget-API verwenden, um eine Antwort vomFuture-Objekt abzufragen:

AsynchronousSocketChannel worker = future.get();

Dieser Anruf wird bei Bedarf blockiert, um auf eine Verbindungsanforderung von einem Client zu warten. Optional können wir eine Zeitüberschreitung angeben, wenn wir nicht ewig warten möchten:

AsynchronousSocketChannel worker = acceptFuture.get(10, TimeUnit.SECONDS);

Nachdem der obige Aufruf zurückgegeben wurde und die Operation erfolgreich war, können wir eine Schleife erstellen, in der wir auf eingehende Nachrichten warten und diese an den Client zurücksenden.

Erstellen wir eine Methode namensrunServer, in der wir warten und alle eingehenden Nachrichten verarbeiten:

public void runServer() {
    clientChannel = acceptResult.get();
    if ((clientChannel != null) && (clientChannel.isOpen())) {
        while (true) {
            ByteBuffer buffer = ByteBuffer.allocate(32);
            Future readResult  = clientChannel.read(buffer);

            // perform other computations

            readResult.get();

            buffer.flip();
            Future writeResult = clientChannel.write(buffer);

            // perform other computations

            writeResult.get();
            buffer.clear();
        }
        clientChannel.close();
        serverChannel.close();
    }
}

Innerhalb der Schleife erstellen wir lediglich einen Puffer, aus dem je nach Operation gelesen und beschrieben werden kann.

Jedes Mal, wenn wir lesen oder schreiben, können wir mit der Ausführung eines anderen Codes fortfahren. Wenn wir bereit sind, das Ergebnis zu verarbeiten, rufen wir die APIget()für das ObjektFutureauf.

Um den Server zu starten, rufen wir seinen Konstruktor und dann dierunServer-Methode inmain auf:

public static void main(String[] args) {
    AsyncEchoServer server = new AsyncEchoServer();
    server.runServer();
}

3. Der Server mitCompletionHandler

In diesem Abschnitt erfahren Sie, wie Sie denselben Server mit dem Ansatz vonCompletionHandlerund nicht mit dem Ansatz vonFutureimplementieren.

Innerhalb des Konstruktors erstellen wir einAsynchronousServerSocketChannel und binden es wie zuvor an eine lokale Adresse:

serverChannel = AsynchronousServerSocketChannel.open();
InetSocketAddress hostAddress = new InetSocketAddress("localhost", 4999);
serverChannel.bind(hostAddress);

Als Nächstes erstellen wir noch im Konstruktor eine while-Schleife, in der wir jede eingehende Verbindung von einem Client akzeptieren. Diese while-Schleife wird ausschließlich fürprevent the server from exiting before establishing a connection with a client verwendet.

Zuprevent the loop from running endlessly rufen wirSystem.in.read() am Ende auf, um die Ausführung zu blockieren, bis eine eingehende Verbindung aus dem Standardeingabestream gelesen wird:

while (true) {
    serverChannel.accept(
      null, new CompletionHandler() {

        @Override
        public void completed(
          AsynchronousSocketChannel result, Object attachment) {
            if (serverChannel.isOpen()){
                serverChannel.accept(null, this);
            }

            clientChannel = result;
            if ((clientChannel != null) && (clientChannel.isOpen())) {
                ReadWriteHandler handler = new ReadWriteHandler();
                ByteBuffer buffer = ByteBuffer.allocate(32);

                Map readInfo = new HashMap<>();
                readInfo.put("action", "read");
                readInfo.put("buffer", buffer);

                clientChannel.read(buffer, readInfo, handler);
             }
         }
         @Override
         public void failed(Throwable exc, Object attachment) {
             // process error
         }
    });
    System.in.read();
}

Wenn eine Verbindung hergestellt wird, wird die Rückrufmethodecompleted inCompletionHandler der Akzeptanzoperation aufgerufen.

Sein Rückgabetyp ist eine Instanz vonAsynchronousSocketChannel. Wenn der Server-Socket-Kanal noch offen ist, rufen wir dieaccept-API erneut auf, um uns auf eine weitere eingehende Verbindung vorzubereiten und denselben Handler wiederzuverwenden.

Als Nächstes weisen wir den zurückgegebenen Socket-Kanal einer globalen Instanz zu. Wir überprüfen dann, dass es nicht null ist und dass es offen ist, bevor wir Operationen daran ausführen.

Der Punkt, an dem wir Lese- und Schreiboperationen starten können, befindet sich in der Rückruf-API voncompleteddes Handlers der Operationaccept. Dieser Schritt ersetzt den vorherigen Ansatz, bei dem der Kanal mit derget-API abgefragt wurde.

Beachten Sie, dassthe server will no longer exit after a connection has been established ist, es sei denn, wir schließen es explizit.

Beachten Sie auch, dass wir eine separate innere Klasse für die Verarbeitung von Lese- und Schreibvorgängen erstellt haben. ReadWriteHandler. Wir werden sehen, wie nützlich das Attachment-Objekt an dieser Stelle ist.

Schauen wir uns zunächst die KlasseReadWriteHandleran:

class ReadWriteHandler implements
  CompletionHandler> {

    @Override
    public void completed(
      Integer result, Map attachment) {
        Map actionInfo = attachment;
        String action = (String) actionInfo.get("action");

        if ("read".equals(action)) {
            ByteBuffer buffer = (ByteBuffer) actionInfo.get("buffer");
            buffer.flip();
            actionInfo.put("action", "write");

            clientChannel.write(buffer, actionInfo, this);
            buffer.clear();

        } else if ("write".equals(action)) {
            ByteBuffer buffer = ByteBuffer.allocate(32);

            actionInfo.put("action", "read");
            actionInfo.put("buffer", buffer);

            clientChannel.read(buffer, actionInfo, this);
        }
    }

    @Override
    public void failed(Throwable exc, Map attachment) {
        //
    }
}

Der generische Typ unseres Anhangs in der KlasseReadWriteHandlerist eine Karte. Wir müssen speziell zwei wichtige Parameter durchlaufen - die Art der Operation (Aktion) und den Puffer.

Als nächstes werden wir sehen, wie diese Parameter verwendet werden.

Die erste Operation, die wir ausführen, istread, da dies ein Echoserver ist, der nur auf Clientnachrichten reagiert. Innerhalb der RückrufmethodeReadWriteHandlercompletedrufen wir die angehängten Daten ab und entscheiden, was entsprechend zu tun ist.

Wenn die Operationreadabgeschlossen ist, rufen wir den Puffer ab, ändern den Aktionsparameter des Anhangs und führen sofort eine Operationwriteaus, um die Nachricht an den Client weiterzuleiten.

Wenn es sich um einewrite-Operation handelt, die gerade abgeschlossen wurde, rufen wir dieread-API erneut auf, um den Server auf den Empfang einer weiteren eingehenden Nachricht vorzubereiten.

4. Der Kunde

Nach dem Einrichten des Servers können wir den Client jetzt einrichten, indem wir die APIopenfür die KlasseAsyncronousSocketChannelaufrufen. Dieser Aufruf erstellt eine neue Instanz des Client-Socket-Kanals, über den wir dann eine Verbindung zum Server herstellen:

AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
InetSocketAddress hostAddress = new InetSocketAddress("localhost", 4999)
Future future = client.connect(hostAddress);

Die Operationconnectgibt bei Erfolg nichts zurück. Wir können jedoch weiterhin das ObjektFutureverwenden, um den Status der asynchronen Operation zu überwachen.

Rufen Sie dieget-API auf, um auf die Verbindung zu warten:

future.get()

Nach diesem Schritt können wir beginnen, Nachrichten an den Server zu senden und dafür Echos zu empfangen. Die MethodesendMessageieht folgendermaßen aus:

public String sendMessage(String message) {
    byte[] byteMsg = new String(message).getBytes();
    ByteBuffer buffer = ByteBuffer.wrap(byteMsg);
    Future writeResult = client.write(buffer);

    // do some computation

    writeResult.get();
    buffer.flip();
    Future readResult = client.read(buffer);

    // do some computation

    readResult.get();
    String echo = new String(buffer.array()).trim();
    buffer.clear();
    return echo;
}

5. Der Test

Um zu bestätigen, dass unsere Server- und Clientanwendungen erwartungsgemäß funktionieren, können wir einen Test verwenden:

@Test
public void givenServerClient_whenServerEchosMessage_thenCorrect() {
    String resp1 = client.sendMessage("hello");
    String resp2 = client.sendMessage("world");

    assertEquals("hello", resp1);
    assertEquals("world", resp2);
}

6. Fazit

In diesem Artikel haben wir die Java NIO.2-APIs für asynchrone Socket-Kanäle untersucht. Mit diesen neuen APIs konnten wir den Prozess des Aufbaus eines Servers und eines Clients schrittweise durchführen.

Sie können auf den vollständigen Quellcode für diesen Artikel inGithub project zugreifen.