Guide du 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 examinerons les classes AsynchronousServerSocketChannel et AsynchronousSocketChannel qui sont les classes clés utilisées respectivement dans l'implémentation du serveur et du client.

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

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

import java.nio.channels.*;

2. Le serveur d' avenir

Une instance d' AsynchronousServerSocketChannel 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 éventuellement choisir un port:

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

Nous pourrions tout aussi bien avoir passé null pour qu'il utilise une adresse locale et se lie à un port arbitraire:

server.bind(null);

Une fois liée, l' API d' acceptation est utilisée pour lancer l'acceptation des connexions au socket du canal:

Future acceptFuture = server.accept();

Comme pour les opérations de canal asynchrone, l'appel ci-dessus revient immédiatement et l'exécution se poursuit.

Ensuite, nous pouvons utiliser l' API get pour demander une réponse de l' objet Future :

AsynchronousSocketChannel worker = future.get();

Cet appel bloquera si nécessaire pour attendre une demande de connexion d'un client. En option, nous pouvons spécifier un délai si nous ne voulons pas attendre indéfiniment:

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

Après le retour de l'appel ci-dessus et l'opération réussie, nous pouvons créer une boucle dans laquelle nous écoutons les messages entrants et les renvoyons au client.

Créons une méthode appelée runServer 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(); } }

À l'intérieur de 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 effectuons 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' API get () sur l' objet Future .

Pour démarrer le serveur, nous appelons son constructeur puis la méthode runServer dans main :

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

3. Le serveur avec CompletionHandler

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

À l'intérieur du constructeur, nous créons un AsynchronousServerSocketChannel 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 à l'intérieur du constructeur, nous créons une boucle while dans laquelle nous acceptons toute connexion entrante d'un client. Cette boucle while est utilisée strictement pour empêcher le serveur de se fermer avant d'établir une connexion avec un client .

Pour empêcher la boucle de s'exécuter indéfiniment , nous appelons System.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 rappel terminée dans le CompletionHandler de l'opération d'acceptation est appelée.

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

Ensuite, nous affectons le canal de socket retourné à 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 rappel terminée du gestionnaire de l' opération d' acceptation . Cette étape remplace l'approche précédente où nous avons interrogé le canal avec l' API get .

Notez que le serveur ne se fermera plus une fois la connexion établie, sauf si nous la 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'objet de pièce jointe est utile à ce stade.

Tout d'abord, examinons la classe ReadWriteHandler :

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 classe ReadWriteHandler 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 une lecture car il s'agit d'un serveur d'écho qui ne réagit qu'aux messages clients. A l' intérieur du ReadWriteHandler de terminé méthode de rappel, nous récupérons les données jointes et de décider ce qu'il faut faire en conséquence.

S'il s'agit d'une opération de lecture 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ération d' écriture pour renvoyer le message au client.

If it's a write operation which has just completed, we call the read API again to prepare the server to receive another incoming message.

4. The Client

After setting up the server, we can now set up the client by calling the open API on the AsyncronousSocketChannel class. This call creates a new instance of the client socket channel which we then use to make a connection to the server:

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

The connect operation returns nothing on success. However, we can still use the Future object to monitor the state of the asynchronous operation.

Let's call the get API to await connection:

future.get()

Après cette étape, nous pouvons commencer à envoyer des messages au serveur et à recevoir des échos pour le même. La méthode sendMessage 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 confirmer 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 asynchrone Java NIO.2. Nous avons pu suivre le processus de création d'un serveur et d'un client avec ces nouvelles API.

Vous pouvez accéder au code source complet de cet article dans le projet Github.