Una guida al canale socket asincrono NIO2

1. Panoramica

In questo articolo, dimostreremo come creare un semplice server e il suo client utilizzando le API del canale Java 7 NIO.2.

Esamineremo le classi AsynchronousServerSocketChannel e AsynchronousSocketChannel che sono le classi chiave utilizzate rispettivamente nell'implementazione del server e del client.

Se non conosci le API del canale NIO.2, abbiamo un articolo introduttivo su questo sito. Puoi leggerlo seguendo questo link.

Tutte le classi necessarie per utilizzare le API del canale NIO.2 sono raggruppate nel pacchetto java.nio.channels :

import java.nio.channels.*;

2. Il server con futuro

Un'istanza di AsynchronousServerSocketChannel viene creata chiamando l'API aperta statica sulla sua classe:

AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open();

Un canale socket del server asincrono appena creato è aperto ma non ancora associato, quindi dobbiamo associarlo a un indirizzo locale e, facoltativamente, scegliere una porta:

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

Potremmo altrettanto bene passare in null in modo che utilizzi un indirizzo locale e si colleghi a una porta arbitraria:

server.bind(null);

Una volta associato, l' API di accettazione viene utilizzata per avviare l'accettazione delle connessioni al socket del canale:

Future acceptFuture = server.accept();

Come accade per le operazioni di canale asincrone, la chiamata precedente ritorna immediatamente e l'esecuzione continua.

Successivamente, possiamo utilizzare l' API get per richiedere una risposta dall'oggetto Future :

AsynchronousSocketChannel worker = future.get();

Questa chiamata si bloccherà se necessario per attendere una richiesta di connessione da un client. Facoltativamente, possiamo specificare un timeout se non vogliamo aspettare per sempre:

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

Dopo che la chiamata di cui sopra è tornata e l'operazione è andata a buon fine, possiamo creare un ciclo all'interno del quale ascoltiamo i messaggi in arrivo e li rispediamo al client.

Creiamo un metodo chiamato runServer all'interno del quale faremo l'attesa ed elaboreremo eventuali messaggi in arrivo:

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

All'interno del ciclo, tutto ciò che facciamo è creare un buffer da cui leggere e scrivere a seconda dell'operazione.

Quindi, ogni volta che eseguiamo una lettura o una scrittura, possiamo continuare a eseguire qualsiasi altro codice e quando siamo pronti per elaborare il risultato, chiamiamo l' API get () sull'oggetto Future .

Per avviare il server, chiamiamo il suo costruttore e quindi il metodo runServer all'interno di main :

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

3. Il server con CompletionHandler

In questa sezione vedremo come implementare lo stesso server utilizzando l' approccio CompletionHandler piuttosto che un approccio Future .

All'interno del costruttore, creiamo un AsynchronousServerSocketChannel e lo colleghiamo a un indirizzo locale nello stesso modo in cui facevamo prima:

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

Successivamente, sempre all'interno del costruttore, creiamo un ciclo while all'interno del quale accettiamo qualsiasi connessione in entrata da un client. Questo ciclo while viene utilizzato rigorosamente per impedire la chiusura del server prima di stabilire una connessione con un client .

Per evitare che il ciclo venga eseguito all'infinito , chiamiamo System.in.read () alla sua fine per bloccare l'esecuzione fino a quando una connessione in entrata non viene letta dal flusso di input 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(); }

Quando viene stabilita una connessione, viene chiamato il metodo di callback completato nel CompletionHandler dell'operazione di accettazione.

Il tipo restituito è un'istanza di AsynchronousSocketChannel . Se il canale socket del server è ancora aperto, chiamiamo nuovamente l'API di accettazione per prepararci per un'altra connessione in entrata riutilizzando lo stesso gestore.

Successivamente, assegniamo il canale socket restituito a un'istanza globale. Verifichiamo quindi che non sia nullo e che sia aperto prima di eseguire operazioni su di esso.

Il punto in cui possiamo iniziare le operazioni di lettura e scrittura è all'interno dell'API di callback completata del gestore dell'operazione di accettazione . Questo passaggio sostituisce l'approccio precedente in cui abbiamo eseguito il polling del canale con l' API get .

Si noti che il server non verrà più chiuso dopo che è stata stabilita una connessione a meno che non venga chiusa esplicitamente.

Si noti inoltre che abbiamo creato una classe interna separata per la gestione delle operazioni di lettura e scrittura; ReadWriteHandler . Vedremo come l'oggetto allegato torna utile a questo punto.

Per prima cosa, diamo un'occhiata alla 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) { // } }
    

Il tipo generico del nostro allegato nella classe ReadWriteHandler è una mappa. Abbiamo specificamente bisogno di passare attraverso di essa due parametri importanti: il tipo di operazione (azione) e il buffer.

Successivamente, vedremo come vengono utilizzati questi parametri.

La prima operazione che eseguiamo è una lettura poiché questo è un server echo che reagisce solo ai messaggi del client. All'interno del ReadWriteHandler s' completato metodo di callback, recuperiamo i dati allegati e decidere cosa fare di conseguenza.

Se si tratta di un'operazione di lettura completata, recuperiamo il buffer, cambiamo il parametro di azione dell'allegato ed eseguiamo subito un'operazione di scrittura per far eco il messaggio al 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()

Dopo questo passaggio, possiamo iniziare a inviare messaggi al server e ricevere echi per lo stesso. Il metodo sendMessage ha questo aspetto:

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

Per confermare che le nostre applicazioni server e client stanno funzionando secondo le aspettative, possiamo utilizzare 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. Conclusione

In questo articolo, abbiamo esplorato le API del canale socket asincrono Java NIO.2. Siamo stati in grado di completare il processo di creazione di un server e di un client con queste nuove API.

È possibile accedere al codice sorgente completo di questo articolo nel progetto Github.