Introduzione a Java NIO Selector

1. Panoramica

In questo articolo, esploreremo le parti introduttive del componente Selector di Java NIO .

Un selettore fornisce un meccanismo per monitorare uno o più canali NIO e riconoscere quando uno o più diventano disponibili per il trasferimento dei dati.

In questo modo, un singolo thread può essere utilizzato per gestire più canali e quindi più connessioni di rete.

2. Perché utilizzare un selettore?

Con un selettore, possiamo usare un thread invece di diversi per gestire più canali. Il cambio di contesto tra i thread è costoso per il sistema operativo e, inoltre, ogni thread occupa memoria.

Pertanto, meno thread utilizziamo, meglio è. Tuttavia, è importante ricordare che i sistemi operativi e le CPU moderni continuano a migliorare nel multitasking , quindi i costi generali del multi-threading continuano a diminuire nel tempo.

Ci occuperemo qui di come possiamo gestire più canali con un singolo thread utilizzando un selettore.

Nota anche che i selettori non ti aiutano solo a leggere i dati; possono anche ascoltare le connessioni di rete in entrata e scrivere dati su canali lenti.

3. Configurazione

Per utilizzare il selettore, non abbiamo bisogno di alcuna configurazione speciale. Tutte le classi di cui abbiamo bisogno sono il pacchetto java.nio principale e dobbiamo solo importare ciò di cui abbiamo bisogno.

Dopodiché, possiamo registrare più canali con un oggetto selettore. Quando l'attività di I / O si verifica su uno dei canali, il selettore ci avvisa. È così che possiamo leggere da un gran numero di origini dati da un singolo thread.

Qualsiasi canale che registriamo con un selettore deve essere una sottoclasse di SelectableChannel . Questi sono un tipo speciale di canali che possono essere messi in modalità non bloccante.

4. Creazione di un selettore

Un selettore può essere creato invocando il metodo di apertura statica della classe Selector , che utilizzerà il provider del selettore predefinito del sistema per creare un nuovo selettore:

Selector selector = Selector.open();

5. Registrazione dei canali selezionabili

Affinché un selettore possa monitorare qualsiasi canale, dobbiamo registrare questi canali con il selettore. Lo facciamo invocando il metodo di registro del canale selezionabile.

Ma prima che un canale sia registrato con un selettore, deve essere in modalità non bloccante:

channel.configureBlocking(false); SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

Ciò significa che non possiamo usare FileChannel con un selettore poiché non possono essere commutati in modalità non bloccante come facciamo con i canali socket.

Il primo parametro è l' oggetto Selector che abbiamo creato in precedenza, il secondo parametro definisce un set di interessi , ovvero quali eventi siamo interessati ad ascoltare nel canale monitorato, tramite il selettore.

Esistono quattro diversi eventi che possiamo ascoltare, ciascuno rappresentato da una costante nella classe SelectionKey :

  • Connetti : quando un client tenta di connettersi al server. Rappresentato da SelectionKey.OP_CONNECT
  • Accetta : quando il server accetta una connessione da un client. Rappresentato da SelectionKey.OP_ACCEPT
  • Lettura : quando il server è pronto per leggere dal canale. Rappresentato da SelectionKey.OP_READ
  • Scrivi : quando il server è pronto per scrivere sul canale. Rappresentato da SelectionKey.OP_WRITE

L'oggetto restituito SelectionKey rappresenta la registrazione del canale selezionabile con il selettore. Lo esamineremo ulteriormente nella sezione seguente.

6. L' oggetto SelectionKey

Come abbiamo visto nella sezione precedente, quando registriamo un canale con un selettore, otteniamo un oggetto SelectionKey . Questo oggetto contiene i dati che rappresentano la registrazione del canale.

Contiene alcune proprietà importanti che dobbiamo comprendere bene per poter utilizzare il selettore sul canale. Esamineremo queste proprietà nelle seguenti sottosezioni.

6.1. Il set di interessi

Un insieme di interessi definisce l'insieme di eventi a cui vogliamo che il selettore presti attenzione su questo canale. È un valore intero; possiamo ottenere queste informazioni nel modo seguente.

In primo luogo, abbiamo il set interessi restituito dal SelectionKey s' interestOps metodo. Quindi abbiamo la costante evento in SelectionKey che abbiamo esaminato in precedenza.

Quando eseguiamo AND questi due valori, otteniamo un valore booleano che ci dice se l'evento viene guardato o meno:

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. Il Ready Set

Il set pronto definisce il set di eventi per cui il canale è pronto. È anche un valore intero; possiamo ottenere queste informazioni nel modo seguente.

Abbiamo il set pronto restituito da SelectionKey s' readyOps metodo. Quando eseguiamo AND su questo valore con le costanti degli eventi come abbiamo fatto nel caso dell'interesse impostato, otteniamo un valore booleano che rappresenta se il canale è pronto per un particolare valore o meno.

Un altro modo alternativo e più breve per farlo è usare i metodi convenienti di SelectionKey per questo stesso scopo:

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

6.3. Il canale

L'accesso al canale che si sta guardando dall'oggetto SelectionKey è molto semplice. Chiamiamo semplicemente il metodo del canale :

Channel channel = key.channel();

6.4. Il selettore

Proprio come ottenere un canale, è molto facile ottenere l' oggetto Selector dall'oggetto SelectionKey :

Selector selector = key.selector();

6.5. Collegamento di oggetti

Possiamo allegare un oggetto a una SelectionKey. A volte potremmo voler dare a un canale un ID personalizzato o allegare qualsiasi tipo di oggetto Java di cui potremmo voler tenere traccia.

Attaccare oggetti è un modo pratico per farlo. Ecco come collegare e ottenere oggetti da una SelectionKey :

key.attach(Object); Object object = key.attachment();

In alternativa, possiamo scegliere di allegare un oggetto durante la registrazione del canale. Lo aggiungiamo come terzo parametro al metodo di registro del canale , in questo modo:

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

7. Selezione tasto canale

Finora, abbiamo esaminato come creare un selettore, registrare canali su di esso e ispezionare le proprietà dell'oggetto SelectionKey che rappresenta la registrazione di un canale a un selettore.

Questa è solo metà del processo, ora dobbiamo eseguire un processo continuo di selezione del set pronto che abbiamo esaminato in precedenza. Facciamo la selezione utilizzando il metodo di selezione del selettore , in questo modo:

int channels = selector.select();

This method blocks until at least one channel is ready for an operation. The integer returned represents the number of keys whose channels are ready for an operation.

Next, we usually retrieve the set of selected keys for processing:

Set selectedKeys = selector.selectedKeys();

The set we have obtained is of SelectionKey objects, each key represents a registered channel which is ready for an operation.

After this, we usually iterate over this set and for each key, we obtain the channel and perform any of the operations that appear in our interest set on it.

During the lifetime of a channel, it may be selected several times as its key appears in the ready set for different events. This is why we must have a continuous loop to capture and process channel events as and when they occur.

8. Complete Example

To cement the knowledge we have gained in the previous sections, we're going to build a complete client-server example.

For ease of testing out our code, we'll build an echo server and an echo client. In this kind of setup, the client connects to the server and starts sending messages to it. The server echoes back messages sent by each client.

When the server encounters a specific message, such as end, it interprets it as the end of the communication and closes the connection with the client.

8.1. The Server

Here is our code for EchoServer.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"); } else { 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(); } }

This is what is happening; we create a Selector object by calling the static open method. We then create a channel also by calling its static open method, specifically a ServerSocketChannel instance.

This is because ServerSocketChannel is selectable and good for a stream-oriented listening socket.

We then bind it to a port of our choice. Remember we said earlier that before registering a selectable channel to a selector, we must first set it to non-blocking mode. So next we do this and then register the channel to the selector.

We don't need the SelectionKey instance of this channel at this stage, so we will not remember it.

Java NIO uses a buffer-oriented model other than a stream-oriented model. So socket communication usually takes place by writing to and reading from a buffer.

We, therefore, create a new ByteBuffer which the server will be writing to and reading from. We initialize it to 256 bytes, it's just an arbitrary value, depending on how much data we plan to transfer to and fro.

Finally, we perform the selection process. We select the ready channels, retrieve their selection keys, iterate over the keys and perform the operations for which each channel is ready.

We do this in an infinite loop since servers usually need to keep running whether there is an activity or not.

The only operation a ServerSocketChannel can handle is an ACCEPT operation. When we accept the connection from a client, we obtain a SocketChannel object on which we can do read and writes. We set it to non-blocking mode and register it for a READ operation to the selector.

During one of the subsequent selections, this new channel will become read-ready. We retrieve it and read it contents into the buffer. True to it's as an echo server, we must write this content back to the client.

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

We finally set the buffer to write mode by calling the flip method and simply write to it.

The start() method is defined so that the echo server can be started as a separate process during unit testing.

8.2. The Client

Here is our code for EchoClient.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; } }

The client is simpler than the server.

We use a singleton pattern to instantiate it inside the start static method. We call the private constructor from this method.

In the private constructor, we open a connection on the same port on which the server channel was bound and still on the same host.

We then create a buffer to which we can write and from which we can read.

Finally, we have a sendMessage method which reads wraps any string we pass to it into a byte buffer which is transmitted over the channel to the server.

Quindi leggiamo dal canale client per ottenere il messaggio inviato dal server. Restituiamo questo come l'eco del nostro messaggio.

8.3. Test

All'interno di una classe chiamata EchoTest.java , creeremo un test case che avvia il server, invia messaggi al server e passa solo quando gli stessi messaggi vengono ricevuti dal server. Come passaggio finale, lo scenario di test arresta il server prima del completamento.

Ora possiamo eseguire il test:

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. Conclusione

In questo articolo, abbiamo coperto l'utilizzo di base del componente Java NIO Selector.

Il codice sorgente completo e tutti i frammenti di codice per questo articolo sono disponibili nel mio progetto GitHub.