Guide sur le canal de socket asynchrone NIO2

Guide sur le canal de socket asynchrone NIO2

1. Vue d'ensemble

Dans cet article, nous allons montrer comment créer un serveur simple et son client à l'aide des API de canal Java 7 NIO.2.

Nous allons examiner les classesAsynchronousServerSocketChannel etAsynchronousSocketChannel qui sont les classes clés utilisées respectivement dans l'implémentation du serveur et du client.

Si vous êtes nouveau sur les API de canal NIO.2, nous avons un article d'introduction sur ce site. Vous pouvez le lire en suivant celink.

Toutes les classes nécessaires pour utiliser les API de canal NIO.2 sont regroupées dans le packagejava.nio.channels:

import java.nio.channels.*;

2. Le serveur avecFuture

Une instance deAsynchronousServerSocketChannel est créée en appelant l'API statique ouverte sur sa classe:

AsynchronousServerSocketChannel server
  = AsynchronousServerSocketChannel.open();

Un canal de socket de serveur asynchrone nouvellement créé est ouvert mais pas encore lié. Nous devons donc le lier à une adresse locale et choisir éventuellement un port:

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

Nous aurions tout aussi bien pu passer à null pour qu'il utilise une adresse locale et se lie à un port arbitraire:

server.bind(null);

Une fois liée, l'APIaccept est utilisée pour initier l'acceptation des connexions au socket du canal:

Future acceptFuture = server.accept();

Comme dans le cas d'opérations sur canal asynchrone, l'appel ci-dessus est renvoyé immédiatement et l'exécution se poursuit.

Ensuite, nous pouvons utiliser l'APIget pour demander une réponse de l'objetFuture:

AsynchronousSocketChannel worker = future.get();

Cet appel sera bloqué si nécessaire pour attendre une demande de connexion d'un client. Si vous ne souhaitez pas attendre indéfiniment, nous pouvons spécifier un délai d’expiration:

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

Une fois que l'appel ci-dessus est revenu et que l'opération a réussi, nous pouvons créer une boucle dans laquelle nous écoutons les messages entrants et les renvoyons au client.

Créons une méthode appeléerunServer dans laquelle nous ferons l’attente et traiterons tous les messages entrants:

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

Dans la boucle, tout ce que nous faisons est de créer un tampon pour lire et écrire en fonction de l'opération.

Ensuite, à chaque fois que nous faisons une lecture ou une écriture, nous pouvons continuer à exécuter tout autre code et lorsque nous sommes prêts à traiter le résultat, nous appelons l'APIget() sur l'objetFuture.

Pour démarrer le serveur, nous appelons son constructeur puis la méthoderunServer à l'intérieur demain:

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

3. Le serveur avecCompletionHandler

Dans cette section, nous verrons comment implémenter le même serveur en utilisant l'approcheCompletionHandler plutôt qu'une approcheFuture.

À l'intérieur du constructeur, nous créons unAsynchronousServerSocketChannel et le lions à une adresse locale de la même manière que nous l'avons fait auparavant:

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

Ensuite, toujours dans le constructeur, nous créons une boucle while au sein de laquelle nous acceptons toute connexion entrante provenant d'un client. Cette boucle while est utilisée strictement pourprevent the server from exiting before establishing a connection with a client.

Pourprevent the loop from running endlessly, nous appelonsSystem.in.read() à sa fin pour bloquer l'exécution jusqu'à ce qu'une connexion entrante soit lue à partir du flux d'entrée standard:

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

Lorsqu'une connexion est établie, la méthode de rappelcompleted dans lesCompletionHandler de l'opération d'acceptation est appelée.

Son type de retour est une instance deAsynchronousSocketChannel. Si le canal de socket du serveur est toujours ouvert, nous appelons à nouveau l'APIaccept pour nous préparer à une autre connexion entrante tout en réutilisant le même gestionnaire.

Ensuite, nous affectons le canal de socket renvoyé à une instance globale. Nous vérifions ensuite qu'il n'est pas nul et qu'il est ouvert avant d'effectuer des opérations dessus.

Le point auquel nous pouvons commencer les opérations de lecture et d'écriture se trouve à l'intérieur de l'API de rappelcompleted du gestionnaire d'opérationaccept. Cette étape remplace l'approche précédente où nous avons interrogé le canal avec l'APIget.

Notez quethe server will no longer exit after a connection has been established sauf si nous le fermons explicitement.

Notez également que nous avons créé une classe interne distincte pour gérer les opérations de lecture et d'écriture; ReadWriteHandler. Nous verrons comment l’attachement est utile à ce stade.

Examinons d'abord la classeReadWriteHandler:

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) {
        //
    }
}

Le type générique de notre pièce jointe dans la classeReadWriteHandler est une carte. Nous devons spécifiquement y passer deux paramètres importants: le type d'opération (action) et le tampon.

Ensuite, nous verrons comment ces paramètres sont utilisés.

La première opération que nous effectuons est unread car il s'agit d'un serveur d'écho qui ne réagit qu'aux messages clients. Dans la méthode de rappel deReadWriteHandler decompleted, nous récupérons les données jointes et décidons quoi faire en conséquence.

Si c'est une opérationread qui est terminée, nous récupérons le tampon, modifions le paramètre d'action de la pièce jointe et effectuons immédiatement une opérationwrite pour renvoyer le message au client.

Si c'est une opérationwrite qui vient de se terminer, nous appelons à nouveau l'APIread pour préparer le serveur à recevoir un autre message entrant.

4. Le client

Après avoir configuré le serveur, nous pouvons maintenant configurer le client en appelant l'APIopen sur la classeAsyncronousSocketChannel. Cet appel crée une nouvelle instance du canal de socket client que nous utilisons ensuite pour établir une connexion avec le serveur:

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

L'opérationconnect ne renvoie rien en cas de succès. Cependant, nous pouvons toujours utiliser l'objetFuture pour surveiller l'état de l'opération asynchrone.

Appelons l'APIget pour attendre la connexion:

future.get()

Après cette étape, nous pouvons commencer à envoyer des messages au serveur et à recevoir les échos correspondants. La méthodesendMessage ressemble à ceci:

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. Le test

Pour vérifier que nos applications serveur et client fonctionnent conformément aux attentes, nous pouvons utiliser un test:

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

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

6. Conclusion

Dans cet article, nous avons exploré les API de canal de socket asynchrones Java NIO.2. Nous avons été en mesure de créer un serveur et un client avec ces nouvelles API.

Vous pouvez accéder au code source complet de cet article dans leGithub project.