1. Información general
En este artículo, exploraremos las partes introductorias del componente Selector de Java NIO .
Un selector proporciona un mecanismo para monitorear uno o más canales NIO y reconocer cuándo uno o más están disponibles para la transferencia de datos.
De esta manera, se puede utilizar un solo hilo para administrar múltiples canales y, por lo tanto, múltiples conexiones de red.
2. ¿Por qué utilizar un selector?
Con un selector, podemos usar un hilo en lugar de varios para administrar múltiples canales. El cambio de contexto entre subprocesos es costoso para el sistema operativo y, además, cada subproceso ocupa memoria.
Por lo tanto, cuantos menos subprocesos usemos, mejor. Sin embargo, es importante recordar que los sistemas operativos modernos y las CPU siguen mejorando en la multitarea , por lo que los gastos generales de los subprocesos múltiples siguen disminuyendo con el tiempo.
Lo que trataremos aquí es cómo podemos manejar múltiples canales con un solo hilo usando un selector.
Tenga en cuenta también que los selectores no solo le ayudan a leer datos; también pueden escuchar las conexiones de red entrantes y escribir datos en canales lentos.
3. Configuración
Para usar el selector, no necesitamos ninguna configuración especial. Todas las clases que necesitamos son el paquete central java.nio y solo tenemos que importar lo que necesitamos.
Después de eso, podemos registrar varios canales con un objeto selector. Cuando ocurre actividad de E / S en cualquiera de los canales, el selector nos lo notifica. Así es como podemos leer de una gran cantidad de fuentes de datos de un solo hilo.
Cualquier canal que registremos con un selector debe ser una subclase de SelectableChannel . Se trata de un tipo especial de canales que se pueden poner en modo sin bloqueo.
4. Creación de un selector
Se puede crear un selector invocando el método abierto estático de la clase Selector , que utilizará el proveedor de selector predeterminado del sistema para crear un nuevo selector:
Selector selector = Selector.open();
5. Registro de canales seleccionables
Para que un selector pueda monitorear cualquier canal, debemos registrar estos canales con el selector. Hacemos esto invocando el método de registro del canal seleccionable.
Pero antes de que un canal se registre con un selector, debe estar en modo sin bloqueo:
channel.configureBlocking(false); SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
Esto significa que no podemos usar FileChannel s con un selector, ya que no se pueden cambiar al modo sin bloqueo como lo hacemos con los canales de socket.
El primer parámetro es el objeto Selector que creamos anteriormente, el segundo parámetro define un conjunto de interés , es decir , qué eventos estamos interesados en escuchar en el canal monitoreado, a través del selector.
Hay cuatro eventos diferentes que podemos escuchar, cada uno está representado por una constante en la clase SelectionKey :
- Conectar : cuando un cliente intenta conectarse al servidor. Representado por SelectionKey.OP_CONNECT
- Aceptar : cuando el servidor acepta una conexión de un cliente. Representado por SelectionKey.OP_ACCEPT
- Leer : cuando el servidor está listo para leer desde el canal. Representado por SelectionKey.OP_READ
- Escribir : cuando el servidor está listo para escribir en el canal. Representado por SelectionKey.OP_WRITE
El objeto devuelto SelectionKey representa el registro del canal seleccionable con el selector. Lo veremos más a fondo en la siguiente sección.
6. El objeto SelectionKey
Como vimos en la sección anterior, cuando registramos un canal con un selector, obtenemos un objeto SelectionKey . Este objeto contiene datos que representan el registro del canal.
Contiene algunas propiedades importantes que debemos entender bien para poder utilizar el selector en el canal. Veremos estas propiedades en las siguientes subsecciones.
6.1. El conjunto de intereses
Un conjunto de intereses define el conjunto de eventos que queremos que el selector tenga en cuenta en este canal. Es un valor entero; podemos obtener esta información de la siguiente manera.
En primer lugar, tenemos el conjunto interés devuelto por el SelectionKey 's interestOps método. Luego tenemos la constante de evento en SelectionKey que vimos anteriormente.
Cuando hacemos Y estos dos valores, obtenemos un valor booleano que nos dice si el evento está siendo observado o no:
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. El conjunto listo
El conjunto listo define el conjunto de eventos para los que está listo el canal. También es un valor entero; podemos obtener esta información de la siguiente manera.
Tenemos la lista conjunto devuelto por SelectionKey 's readyOps método. Cuando hacemos Y este valor con las constantes de eventos como hicimos en el caso del conjunto de interés, obtenemos un booleano que representa si el canal está listo para un valor particular o no.
Otra forma alternativa y más corta de hacer esto es usar los métodos convenientes de SelectionKey para este mismo propósito:
selectionKey.isAcceptable(); selectionKey.isConnectable(); selectionKey.isReadable(); selectionKey.isWriteable();
6.3. El canal
Acceder al canal que se está viendo desde el objeto SelectionKey es muy simple. Simplemente llamamos al método del canal :
Channel channel = key.channel();
6.4. El selector
Al igual que obtener un canal, es muy fácil obtener el objeto Selector del objeto SelectionKey :
Selector selector = key.selector();
6.5. Adjuntar objetos
Podemos adjuntar un objeto a una SelectionKey. A veces, es posible que deseemos darle a un canal una ID personalizada o adjuntar cualquier tipo de objeto Java que deseemos seguir.
Adjuntar objetos es una forma práctica de hacerlo. Así es como adjunta y obtiene objetos de una SelectionKey :
key.attach(Object); Object object = key.attachment();
Alternativamente, podemos optar por adjuntar un objeto durante el registro del canal. Lo agregamos como un tercer parámetro al método de registro del canal , así:
SelectionKey key = channel.register( selector, SelectionKey.OP_ACCEPT, object);
7. Selección de clave de canal
Hasta ahora, hemos visto cómo crear un selector, registrar canales en él e inspeccionar las propiedades del objeto SelectionKey que representa el registro de un canal en un selector.
Esto es solo la mitad del proceso, ahora tenemos que realizar un proceso continuo de selección del conjunto listo que vimos anteriormente. Lo hacemos mediante la selección del selector de selección de método, de este 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.
Luego leemos del canal del cliente para obtener el mensaje enviado por el servidor. Devolvemos esto como el eco de nuestro mensaje.
8.3. Pruebas
Dentro de una clase llamada EchoTest.java , vamos a crear un caso de prueba que inicia el servidor, envía mensajes al servidor y solo pasa cuando los mismos mensajes se reciben desde el servidor. Como paso final, el caso de prueba detiene el servidor antes de que se complete.
Ahora podemos ejecutar la prueba:
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. Conclusión
En este artículo, hemos cubierto el uso básico del componente Selector de Java NIO.
El código fuente completo y todos los fragmentos de código de este artículo están disponibles en mi proyecto de GitHub.