Conocimiento previo
Sincrónico, asíncrono, bloqueo, sin bloqueo
En primer lugar, estos conceptos son muy fáciles de confundir, pero están involucrados en NIO, así que resumamos.
Sincronización: cuando la llamada de la API regresa, la persona que llama sabe cómo se produce la operación (cuántos bytes realmente se leen/escriben).
Asíncrono: en comparación con la sincronización, la persona que llama no conoce el resultado de la operación cuando la llamada de la API regresa, y la devolución de llamada notificará el resultado más adelante.
Bloqueo: cuando no hay datos para leer o no se pueden escribir todos los datos, suspenda el hilo actual que espera.
Sin bloqueo: al leer, puede leer tanto como los datos que puede leer y luego regresar. Al escribir, puede escribir tanto como los datos que puede escribir y luego regresar.
Para las operaciones de E/S, de acuerdo con los documentos del sitio web oficial de Oracle, el estándar de sincronización y división asincrónica es "si la persona que llama debe esperar a que la operación de E/S complete". Esta "espera de la operación de E/S se complete" no significa que los datos deben leerse o que todos los datos deben escribirse, sino si la persona que llama debe esperar cuando la operación de E/S realmente se lleva a cabo, como el momento en que los datos se transmiten entre el búfer de la pila de protocolo TCP/IP y el búfer JVM.
Por lo tanto, nuestros métodos de lectura () y escritura () comúnmente usados son E/S sincrónicas. La E/S sincrónica se divide en dos modos: bloqueo y no bloqueo. Si se trata de un modo sin bloqueo, se devolverá directamente cuando detecte que no hay datos para leer, y la operación de E/S no se realiza realmente.
En resumen, en Java, en realidad solo hay tres mecanismos: E/S de bloqueo sincrónico, E/S sincronizada sin bloqueo y E/S asincrónica. De lo que estamos hablando a continuación son los dos primeros. JDK1.7 ha comenzado a introducir E/S asíncrono, que se llama NIO.2.
IO tradicional
Sabemos que la aparición de una nueva tecnología siempre va acompañada de mejoras y mejoras, y también lo es la aparición de Javanio.
La E/S tradicional está bloqueando la E/S, y el principal problema es el desperdicio de los recursos del sistema. Por ejemplo, para leer los datos de una conexión TCP, llamamos al método Read () de InputStream, que hará que el hilo actual se suspenda y no se despertará hasta que lleguen los datos. El hilo ocupa recursos de memoria (pila de subprocesos de almacenamiento) durante el período de llegada de datos, pero no hace nada. Esto es lo que dice el dicho, ocupando el pozo y no la caca. Para leer los datos de otras conexiones, tenemos que iniciar otro hilo. Esto puede estar bien cuando no hay muchas conexiones concurrentes, pero cuando el número de conexiones alcanza una determinada escala, una gran cantidad de hilos consumirán recursos de memoria. Por otro lado, la conmutación de subprocesos requiere cambiar el estado del procesador, como los valores de los contadores de programas y los registros, por lo que cambiar entre una gran cantidad de subprocesos también es un desperdicio de recursos.
Con el desarrollo de la tecnología, los sistemas operativos modernos proporcionan nuevos mecanismos de E/S que pueden evitar este desperdicio de recursos. Basado en esto, Javanio nació, y la característica representativa de NIO es E/S sin bloqueo. Inmediatamente después, encontramos que simplemente usar E/S sin bloqueo no puede resolver el problema, porque en el modo no bloqueado, el método Read () volverá inmediatamente cuando no se lean los datos. No sabemos cuándo llegarán los datos, por lo que solo podemos seguir llamando al método Read () para intentarlo nuevamente. Obviamente, esto es un desperdicio de recursos de CPU. A partir de lo siguiente, podemos saber que el componente selector nació para resolver este problema.
Componentes centrales de Javanio
1. canal
concepto
Todas las operaciones de E/S en Javanio se basan en objetos de canal, al igual que las operaciones de transmisión se basan en objetos de transmisión, por lo que es necesario comprender primero qué es el canal. El siguiente contenido se extrae de la documentación de JDK1.8
Achannel representa la conexión del anexo a la anexo como un componente de oroprograma que se puede realizar en una operación distintiva de I/O, por ejemplo, que se puede realizar en una operación de I/O distintiva ormoria.
Del contenido anterior, podemos ver que un canal representa una conexión con una determinada entidad, que puede ser un archivo, un socket de red, etc. En otras palabras, el canal es un puente proporcionado por Javanio para que nuestros programas interactúen con los servicios de E/S subyacentes del sistema operativo.
Los canales son una descripción muy básica y abstracta, interactúan con diferentes servicios de E/S, realizan diferentes operaciones de E/S e implementan diferentes implementaciones. Por lo tanto, los específicos incluyen fileChannel, Socketchannel, etc.
El canal es similar a la transmisión cuando se usa. Puede leer datos en un búfer o escribir datos en el búfer al canal.
Por supuesto, también hay diferencias, que se reflejan principalmente en los siguientes dos puntos:
Se puede leer y escribir un canal, mientras que una transmisión es unidireccional (así que dividido en InputStream y OutputStream)
El canal tiene modo de E/S sin bloqueo
lograr
Las implementaciones de canales más utilizadas en Javanio son las siguientes, y se puede ver que corresponden a clases de operación de E/S tradicionales una por una.
FileChannel: leer y escribir archivos
DatagramChannel: comunicación de red de protocolo UDP
Socketchannel: Comunicación de red de protocolo TCP
ServerSocketchannel: Escuche las conexiones TCP
2. BUFFER
El búfer utilizado en NIO no es una matriz de bytes simple, sino una clase de búfer encapsulado. A través de la API que proporciona, podemos manipular de manera flexible los datos. Echemos un vistazo más de cerca.
En correspondencia con los tipos básicos de Java, NIO proporciona una variedad de tipos de amortiguadores, como Bytebuffer, Charbuffer, Intbuffer, etc. La diferencia es que la longitud de la unidad del búfer es diferente cuando lee y escribe (lectura y escritura en unidades de las variables de tipo correspondientes).
Hay 3 variables muy importantes en el amortiguador. Son la clave para comprender el mecanismo de trabajo del amortiguador, a saber
capacidad (capacidad total)
posición (la posición actual del puntero)
Límite (posición de límite de lectura/escritura)
El método de trabajo del búfer es muy similar a las matrices de caracteres en C. En analogía, la capacidad es la longitud total de la matriz, la posición es la variable de subíndice para que podamos leer/escribir caracteres, y el límite es la posición del carácter final. La situación de las 3 variables al comienzo del amortiguador es la siguiente.
Durante el proceso de lectura/redacción del búfer, la posición se moverá hacia atrás y el límite es el límite del movimiento de posición. No es difícil imaginar que al escribir en un búfer, el límite debe establecerse en el tamaño de la capacidad, y al leer a un búfer, el límite debe establecerse en la posición final real de los datos. (Nota: escribir datos de búfer en el canal es una operación de lectura de búfer, y leer datos del canal al búfer es una operación de escritura de búfer)
Antes de leer/escribir operaciones en un búfer, podemos llamar a algunos métodos auxiliares proporcionados por la clase de búfer para establecer correctamente los valores de posición y límite, principalmente de la siguiente manera
Flip (): Establezca el límite en el valor de la posición y luego establezca la posición en 0. Llamas antes de leer el búfer.
Rewind (): Simplemente establezca la posición 0. Por lo general, se llama antes de releer los datos del búfer, por ejemplo, se usará al leer los datos del mismo búfer y escribirlos a varios canales.
Clear (): Regrese al estado inicial, es decir, el límite es igual a la capacidad, posición establecida a 0. Llame al búfer antes de escribir.
Compact (): Mueva los datos no leídos (datos entre posición y límite) al comienzo del búfer y establezca la posición en la siguiente posición al final de estos datos. De hecho, es equivalente a volver a escribir tal datos en el búfer.
Luego, mire un ejemplo, use fileChannel para leer y escribir archivos de texto, y use este ejemplo para verificar las características legibles y de escritura del canal y el uso básico del búfer (tenga en cuenta que el fileChannel no se puede establecer en modo sin bloqueo).
FileChannel Channel = new RandomAccessFile ("Test.txt", "RW"). GetChannel (); Channel.Position (Channel.Size ()); // Mueva el puntero de archivo a la finalización (Append write) byteBuffer bytebuffer = bytebuffer.allocate (20); // Escribir datos a bufferbytebuffer.put.put ("Helloe, hello, hello, hello, hello, Hello World!/N ".getBytes (StandardCharsets.utf_8)); // buffer -> channelbyteBuffer.flip (); while (byteBuffer.hasRemeining ()) {canal.write (byteBuffer);} canal.position (0); // mueve el puntero de archivo al principio (leí CharsetDecoder decoder = StandardCharSets.utf_8.newDecoder (); // Lea todos los datos bytebuffer.clear (); while (channel.read (bytebuffer)! = -1 || bytebuffer.Position ()> 0) {byteBuffer.flip (); // decode charbuffer.clear (); decoder.decode (byteBuffer, charbuffer, false); system.print (charbuffer.flip (). tostring ();; Puede haber datos restantes} channel.close ();En este ejemplo, se utilizan dos búferes, donde ByteBuffer es el búfer de datos para la lectura y la escritura del canal, y Charbuffer se usa para almacenar caracteres decodificados. El uso de Clear () y Flip () es como se mencionó anteriormente. Cabe señalar que el último método compacto () es, incluso si el tamaño del charbuffer es completamente suficiente para acomodar el bytebuffer de datos decodificados, este compacto () también es esencial. Esto se debe a que la codificación UTF-8 de caracteres chinos comúnmente utilizados representa 3 bytes, por lo que existe una alta probabilidad de que ocurra en el truncamiento medio. Consulte la figura a continuación:
Cuando el decodificador lee 0xe4 al final del búfer, no se puede asignar a un unicode. El tercer parámetro del método decode (), False, se usa para hacer que el decodificador trate los bytes impapaces y los datos posteriores como datos adicionales. Por lo tanto, el método decode () se detendrá aquí, y la posición volverá a la posición 0xe4. De esta manera, el primer byte codificado por la palabra "medio" se deja en el búfer, y debe compacirse en la parte delantera y empalmarse junto con los datos de secuencia correctos y posteriores. Con respecto a la codificación de caracteres, puede consultar " explicación de ANSI, Unicode, BMP, UTF y otros conceptos de codificación "
Por cierto, el CharsetDecoder en el ejemplo también es una nueva característica de Javanio, por lo que debería haber descubierto un poco. Las operaciones de NIO están orientadas al búfer (la E/S tradicional está orientada a la corriente).
En este punto, hemos aprendido sobre el uso básico del canal y el búfer. A continuación, hablaremos sobre componentes importantes de dejar que un hilo administre múltiples canales.
3.Selector
Que es selector
El selector es un componente especial utilizado para recopilar el estado (o eventos) de cada canal. Primero registramos el canal en el selector y establecemos el evento que nos importa, y luego podemos esperar en silencio a que ocurra el evento llamando al método select ().
El canal tiene los siguientes 4 eventos para que podamos escuchar:
Aceptar: hay una conexión aceptable
Conectar: conéctese con éxito
Leer: Hay datos para leer
Escribir: puede escribir datos
Por qué usar Selector
Como se mencionó anteriormente, si usa E/S de bloqueo, debe ser múltiple (un desperdicio de memoria), y si usa E/S sin bloqueo, debe intentarlo constantemente (un consumo de CPU). La aparición de selector resuelve este problema vergonzoso. En el modo sin bloqueo, a través del selector, nuestros hilos solo funcionan para canales listos, y no hay necesidad de intentarlo a ciegas. Por ejemplo, cuando no se alcanzan datos en todos los canales, no se produce ningún evento de lectura, y nuestro hilo se suspenderá en el método select (), renunciando así a los recursos de la CPU.
Cómo usar
Como se muestra a continuación, cree un selector y registre un canal.
Nota: Para registrar el canal en el selector, primero debe establecer el canal en modo no bloqueo, de lo contrario se lanzará una excepción.
Selector selector = selector.open (); canal.configureBlowing (false); selectionKey key = canal.register (selector, selectionKey.op_read);
El segundo parámetro del método registro () se llama "conjunto de intereses", que es el conjunto de eventos que le preocupa. Si le importan múltiples eventos, separarlos con un "bital u operador", p.
SelectionKey.op_read | SelectionKey.op_write
Este método de escritura no se desconoce con él. Se reproduce en lenguajes de programación que admiten operaciones de bits. El uso de una variable entera puede identificar múltiples estados. ¿Cómo se hace? En realidad es muy simple. Por ejemplo, primero predefinió algunas constantes, y sus valores (binarios) son los siguientes
Se puede encontrar que los bits con su valor de 1 están escalonados, por lo que los valores obtenidos después de realizar bit a bit o cálculos en ellos no tienen ambigüedad, y pueden deducirse inversamente de qué variables se calculan. Cómo juzgar, sí, es la operación "bits y". Por ejemplo, ahora hay un valor variable de conjunto de estado de 0011. Solo necesitamos determinar si el valor de "0011 y op_read" es 1 o 0 para determinar si el conjunto contiene el estado OP_READ.
Luego, tenga en cuenta que el método Register () devuelve un objeto SelectionKey, que contiene la información para este registro, y también podemos modificar la información de registro a través de él. Del ejemplo completo a continuación, podemos ver que después de select (), también obtenemos los canales con estado listos obteniendo una colección de keys selections.
Un ejemplo completo
Se han explicado los conceptos y las cosas teóricas (en realidad, después de escribirlos aquí, descubrí que no escribía mucho, lo cual es tan vergonzoso (⊙ˍ⊙)). Echemos un vistazo a un ejemplo completo.
Este ejemplo usa Javanio para implementar un servidor de un solo hilo. La función es muy simple. Escucha la conexión del cliente. Cuando se establece la conexión, lee el mensaje del cliente y responde a un mensaje al cliente.
Cabe señalar que uso el carácter '/0' (un byte con un valor de 0) para identificar el final del mensaje.
Servidor de un solo roscado
public class nioserver {public static void main (string [] args) lanza ioexception {// crea un selectorSelector selector = selector.open (); // inicializar el canal de escucha TCP Serversocketchannel Listenchannel = SERVERSOCKEnchannel.open (); ListenchAnnel.bind (nuevo inetSocketAddress (9999)); ListenchAnnel.configureBlowing (false); // Registrarse en Selector (Escuchar a su evento Aceptar) ListenchAnnel.Register (Selector, SelectionKey.op_accept); // Cree un búfer bytebuffer buffer = bytebuffer.allocate (100); while (true) {selector.select (); // bloque hasta que se escuche un evento que ocurre iterator <selectionKey> keyiter = selector.selectedKeys (). iterator (); // Acceda al canal seleccionado a través de un iterador en el tiempo mientras (keyiter.hasnext ()) {selectionKey key = keyiter.next (); if (key.isacceptable ()) Socketchannel Channel = (((ServerSocketchannel) key.channel ()). Aceptación (); canal.configureBlocking (false); canal.register (selector, selectionKey.op_read); system.out.println ("conexión establecida con [" + canal.getRemoteadeadeadDress () + "]!");} Más if (key.isreadable ()) buffer.clear (); // Leer al final de la transmisión, lo que indica que la conexión TCP se ha desconectado, // por lo tanto, es necesario cerrar el canal o cancelar escuchando el evento de lectura // de lo contrario, se encenderá infinitamente if (((SocketchEnchannel) key.channel ()). Read (buffer) == -1) buffer.flip (); while (buffer.hasremaning ()) {byte b = buffer.get (); if (b == 0) {// /0System.Println () al final del mensaje del cliente; // Respuesta Cliente Buffer.Claear (); Buffer.put ("Hello, Cliente!/0" .getBytes (); buffer.flip (); while (buffer.HasRenerer ()) {((SocketChannel) key.channel()).write(buffer);}} else {System.out.print((char) b);}}}// For events that have been processed, you must manually remove keyIter.remove();}}}}Cliente
Este cliente se usa puramente para las pruebas. Para hacerlo menos difícil, utiliza métodos de escritura tradicionales, y el código es muy corto.
Si necesita ser más riguroso en las pruebas, debe ejecutar una gran cantidad de clientes simultáneamente para contar el tiempo de respuesta del servidor, y no envíe datos inmediatamente después de que se establece la conexión, para que se juegue a las ventajas de E/S sin bloqueo en el servidor.
Public Class Client {public static void main (string [] args) lanza la excepción {Socket Socket = new Socket ("LocalHost", 9999); InputStream is = Socket.getInputStream (); OutputStream OS = Socket.getOutputStream (); // Envía datos al servidor First OS.Write ("Hello, Server!/0" .getBytes (); while ((b = is.read ())! = 0) {system.out.print ((char) b);} system.out.println (); socket.close ();}}Resumir
Lo anterior se trata de una comprensión rápida de los componentes centrales de NIO en Java. Espero que sea útil para todos. Los amigos interesados pueden continuar referiéndose a otros contenidos relevantes de este sitio web. Si hay alguna deficiencia, deje un mensaje para señalarlo. ¡Gracias amigos por su apoyo para este sitio!