Explica el canal de java NIO en detalle

Channel es la segunda gran innovación de java.nio. No son una extensión ni una mejora, sino ejemplos nuevos y excelentes de E / S de Java que proporcionan conexiones directas a los servicios de E / S. El canal se usa para transferir datos de manera eficiente entre el búfer de bytes y la entidad en el otro lado del canal (generalmente un archivo o socket).

Introducción al canal

Los canales son conductos para acceder a los servicios de E / S. La E / S se puede dividir en dos categorías amplias: E / S de archivos y E / S de flujo. Por tanto, no es de extrañar que haya dos tipos de canales correspondientes, son canales de archivo y canales de conexión. Vemos que hay una clase FileChannel y tres clases de canal de socket en la api: SocketChannel, ServerSocketChannel y DatagramChannel.

Los canales se pueden crear de muchas formas. Los canales de socket tienen métodos de fábrica que pueden crear directamente nuevos canales de socket. Pero un objeto FileChannel solo se puede obtener llamando al método getChannel () en un objeto RandomAccessFile, FileInputStream o FileOutputStream abierto. No puede crear un objeto FileChannel directamente.

Primero veamos el uso de FileChannel:

 // 创建文件输出字节流
 FileOutputStream fos = new FileOutputStream("data.txt");
 //得到文件通道
 FileChannel fc = fos.getChannel();
 //往通道写入ByteBuffer
 fc.write(ByteBuffer.wrap("Some text ".getBytes()));
 //关闭流
 fos.close();

 //随机访问文件
 RandomAccessFile raf = new RandomAccessFile("data.txt", "rw");
 //得到文件通道
 fc = raf.getChannel();
 //设置通道的文件位置 为末尾
 fc.position(fc.size()); 
 //往通道写入ByteBuffer
 fc.write(ByteBuffer.wrap("Some more".getBytes()));
 //关闭
 raf.close();

 //创建文件输入流
 FileInputStream fs = new FileInputStream("data.txt");
 //得到文件通道
 fc = fs.getChannel();
 //分配ByteBuffer空间大小
 ByteBuffer buff = ByteBuffer.allocate(BSIZE);
 //从通道中读取ByteBuffer
 fc.read(buff);
 //调用此方法为一系列通道写入或相对获取 操作做好准备
 buff.flip();
 //从ByteBuffer从依次读取字节并打印
 while (buff.hasRemaining()){
  System.out.print((char) buff.get());
 }
 fs.close();

Veamos SocketChannel nuevamente:

 SocketChannel sc = SocketChannel.open( );
 sc.connect (new InetSocketAddress ("somehost", someport)); 
 ServerSocketChannel ssc = ServerSocketChannel.open( ); 
 ssc.socket( ).bind (new InetSocketAddress (somelocalport)); 
 DatagramChannel dc = DatagramChannel.open( );

Puede configurar SocketChannel en modo sin bloqueo. Después de la configuración, puede llamar a connect (), read () y write () en modo asincrónico. Si SocketChannel está en modo sin bloqueo y llama a connect () en este momento, el método puede regresar antes de que se establezca la conexión. Para determinar si la conexión está establecida, puede llamar al método finishConnect (). Me gusta esto:

socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));

while(! socketChannel.finishConnect() ){
 //wait, or do something else...
}

El uso del lado del servidor a menudo considera canales de socket sin bloqueo, porque facilitan la administración de muchos canales de socket al mismo tiempo. Sin embargo, también es beneficioso utilizar uno o varios canales de socket sin bloqueo en el lado del cliente. Por ejemplo, con canales de socket sin bloqueo, los programas GUI pueden enfocarse en las solicitudes de los usuarios y mantener sesiones con uno o más servidores al mismo tiempo. . En muchos programas, el modo sin bloqueo es útil.

Llame al método finishConnect () para completar el proceso de conexión. Este método se puede llamar de forma segura en cualquier momento. Si se llama al método finishConnect () en un objeto SocketChannel en modo sin bloqueo, puede ocurrir una de las siguientes situaciones:

  • Aún no se ha llamado al método connect (). Se generará NoConnectionPendingException.
  • El proceso de establecimiento de la conexión está en curso y aún no se ha completado. Entonces no sucederá nada y el método finishConnect () devolverá inmediatamente falso.
  • Después de llamar al método connect () en modo sin bloqueo, SocketChannel vuelve al modo de bloqueo. Luego, si es necesario, el hilo de llamada se bloqueará hasta que se establezca la conexión, y el método finishConnect () devolverá un valor verdadero. Después de la primera llamada a connect () o la última llamada a finishConnect (), se ha completado el proceso de establecimiento de la conexión. Luego, el estado interno del objeto SocketChannel se actualizará al estado conectado, el método finishConnect () devolverá un valor verdadero y luego el objeto SocketChannel se puede usar para transmitir datos.
  • Se ha establecido la conexión. Entonces no sucederá nada y el método finishConnect () devolverá un valor verdadero.

Los canales de enchufe son seguros para subprocesos. Durante el acceso concurrente, no se requieren medidas especiales para proteger los múltiples subprocesos que inician el acceso, pero solo una operación de lectura y una operación de escritura están en progreso en cualquier momento. Recuerde que los sockets están orientados a la transmisión en lugar de a los paquetes. Pueden garantizar que los bytes enviados llegarán en orden, pero no pueden prometer mantener la agrupación de bytes. Un determinado remitente puede escribir 20 bytes en un conector y el receptor solo recibe 3 bytes cuando llama al método read (). Los 17 bytes restantes todavía están en transmisión. Por esta razón, permitir que varios subprocesos que no cooperan compartan el mismo lado de un socket de flujo no es de ninguna manera una buena opción de diseño.

Finalmente, mire el DatagramChannel:

El último canal de socket es DatagramChannel. Así como SocketChannel corresponde a Socket y ServerSocketChannel corresponde a ServerSocket, cada objeto DatagramChannel también tiene un objeto DatagramSocket asociado. Sin embargo, el patrón de nomenclatura original no se aplica aquí: "DatagramSocketChannel" parece un poco torpe, por lo que se adopta el nombre conciso "DatagramChannel".

Así como SocketChannel simula protocolos de transmisión orientados a la conexión (como TCP / IP), DatagramChannel simula protocolos sin conexión orientados a paquetes (como UDP / IP):

El patrón para crear un DatagramChannel es el mismo que para crear otros canales de socket: llame al método estático open () para crear una nueva instancia. El nuevo DatagramChannel tendrá un objeto DatagramSocket par que se puede obtener llamando al método socket (). El objeto DatagramChannel puede actuar como servidor (escucha) o como cliente (remitente). Si desea que el canal recién creado sea responsable del monitoreo, entonces el canal primero debe estar vinculado a un puerto o una combinación de dirección / puerto. Vincular un DatagramChannel no es diferente de vincular un DatagramSocket normal. Ambos se implementan confiando la API en el objeto de socket del mismo nivel:

 DatagramChannel channel = DatagramChannel.open( );
 DatagramSocket socket = channel.socket( ); 
 socket.bind (new InetSocketAddress (portNumber));

DatagramChannel no tiene conexión. Cada datagrama es una entidad autónoma con su propia dirección de destino y carga útil de datos que no depende de otros datagramas. A diferencia de los sockets orientados a la transmisión, DatagramChannel puede enviar datagramas separados a diferentes direcciones de destino. De manera similar, el objeto DatagramChannel también puede recibir paquetes de datos desde cualquier dirección. Cada datagrama que llega contiene información sobre su procedencia (dirección de origen).

Un DatagramChannel no vinculado todavía puede recibir paquetes de datos. Cuando se crea un socket subyacente, se le asignará un número de puerto generado dinámicamente. El comportamiento de enlace requiere que el puerto asociado con el canal se establezca en un valor específico (este proceso puede implicar controles de seguridad u otras verificaciones). Independientemente de si el canal está vinculado o no, todos los paquetes enviados contienen la dirección de origen (con el número de puerto) del DatagramChannel. Unbound DatagramChannel puede recibir paquetes enviados a su puerto, generalmente un paquete enviado antes del canal de ida y vuelta. Los canales vinculados reciben paquetes enviados al puerto conocido al que están vinculados. El envío o la recepción real de datos se logra a través de los métodos send () y receive ().

Nota: Si el ByteBuffer que proporciona no tiene suficiente espacio libre para almacenar el paquete de datos que está recibiendo, los bytes sin completar se descartarán silenciosamente.

Dispersar / Reunir

Los canales proporcionan una nueva característica importante llamada Scatter / Gather (a veces llamada E / S vectorial). Se refiere a la realización de una operación de E / S simple en múltiples búferes. Para una operación de escritura, los datos se extraen secuencialmente de varios búferes (llamados recopilar) y se envían a lo largo del canal. El búfer en sí no necesita tener la capacidad de recolectar (por lo general, no tienen esta capacidad). El efecto del proceso de recopilación es como que el contenido de todos los búferes se concatenan y almacenan en un búfer grande antes de enviar datos. Para la operación de lectura, los datos leídos del canal se distribuirán secuencialmente (llamado dispersión) a múltiples búferes, llenando cada búfer hasta que se consuman los datos en el canal o el espacio máximo del búfer.

La dispersión / recopilación se utiliza a menudo en situaciones en las que los datos transmitidos deben procesarse por separado. Por ejemplo, al transmitir un mensaje compuesto por un encabezado y un cuerpo del mensaje, puede dispersar el cuerpo del mensaje y el encabezado del mensaje en diferentes búferes, de modo que puede procesar cómodamente el encabezado y el cuerpo del mensaje.

La dispersión de lecturas significa que los datos se leen desde un canal a varios búferes. Descrito en la siguiente figura:

El ejemplo de código es el siguiente:

ByteBuffer header = ByteBuffer.allocateDirect (10); 
ByteBuffer body = ByteBuffer.allocateDirect (80); 
ByteBuffer [] buffers = { header, body }; 
int bytesRead = channel.read (buffers);

La recopilación de escrituras se refiere a la escritura de datos de varios búferes en el mismo canal. Descrito en la siguiente figura:

El ejemplo de código es el siguiente:

 ByteBuffer header = ByteBuffer.allocateDirect (10); 
 ByteBuffer body = ByteBuffer.allocateDirect (80); 
 ByteBuffer [] buffers = { header, body }; 
 channel.write(bufferArray);

Si se usa correctamente, Scatter / Gather puede ser una herramienta extremadamente poderosa. Le permite confiar al sistema operativo para que complete el trabajo duro: separe los datos leídos en varios depósitos o combine diferentes bloques de datos en un todo. Este es un gran logro, porque el sistema operativo ha sido altamente optimizado para realizar este tipo de trabajo. Le ahorra el trabajo de mover datos de un lado a otro, lo que también evita la copia en búfer y reduce la cantidad de código que necesita escribir y depurar. Dado que básicamente combina datos al proporcionar referencias de contenedores de datos, luego cree múltiples referencias de matriz de búfer de acuerdo con diferentes combinaciones, y varios bloques de datos se pueden combinar de diferentes maneras. El siguiente ejemplo ilustra bien este punto:

public class GatheringTest {
 private static final String DEMOGRAPHIC = "output.txt";
 public static void main (String [] argv) throws Exception {
  int reps = 10;
  if (argv.length > 0) {
   reps = Integer.parseInt(argv[0]);
  }
  FileOutputStream fos = new FileOutputStream(DEMOGRAPHIC);
  GatheringByteChannel gatherChannel = fos.getChannel();

  ByteBuffer[] bs = utterBS(reps);

  while (gatherChannel.write(bs) > 0) {
   // 不做操作,让通道把数据输出到文件写完
  }
  System.out.println("Mindshare paradigms synergized to " + DEMOGRAPHIC);
  fos.close();
 }
 private static String [] col1 = { "Aggregate", "Enable", "Leverage",
          "Facilitate", "Synergize", "Repurpose",
          "Strategize", "Reinvent", "Harness"
         };

 private static String [] col2 = { "cross-platform", "best-of-breed", "frictionless",
          "ubiquitous", "extensible", "compelling",
          "mission-critical", "collaborative", "integrated"
         };

 private static String [] col3 = { "methodologies", "infomediaries", "platforms", "schemas", "mindshare", "paradigms", "functionalities", "web services", "infrastructures" };

 private static String newline = System.getProperty ("line.separator");


 private static ByteBuffer [] utterBS (int howMany) throws Exception {
  List list = new LinkedList();
  for (int i = 0; i < howMany; i++) {
   list.add(pickRandom(col1, " "));
   list.add(pickRandom(col2, " "));
   list.add(pickRandom(col3, newline));
  }
  ByteBuffer[] bufs = new ByteBuffer[list.size()];
  list.toArray(bufs);
  return (bufs);
 }
 private static Random rand = new Random( );


 /**
  * 随机生成字符
  * @param strings
  * @param suffix
  * @return
  * @throws Exception
  */
 private static ByteBuffer pickRandom (String [] strings, String suffix) throws Exception {
  String string = strings [rand.nextInt (strings.length)];
  int total = string.length() + suffix.length( );
  ByteBuffer buf = ByteBuffer.allocate (total);
  buf.put (string.getBytes ("US-ASCII"));
  buf.put (suffix.getBytes ("US-ASCII"));
  buf.flip( );
  return (buf);
 }
}

La salida es:

Reinventar los servicios web integrados
Agrupar las mejores plataformas
Aprovechar las plataformas sin fricciones
Reutilizar paradigmas extensibles
Facilitar metodologías ubicuas
Reutilizar metodologías integradas
Facilitar paradigmas de misión crítica
Sinergizar metodologías
convincentes Reinventar funcionalidades convincentes
Facilitar plataformas extensibles

Aunque este resultado no tiene sentido, para nosotros es muy fácil generarlo.

Tubo

El paquete java.nio.channels contiene una clase llamada Pipe. En términos generales, una tubería es un conducto que se utiliza para transmitir datos en una dirección entre dos entidades.
La canalización de Java NIO es una conexión de datos unidireccional entre dos subprocesos. La tubería tiene un canal fuente y un canal sumidero. Los datos se escribirán en el canal receptor y se leerán desde el canal fuente. La clase Pipe crea un par de objetos Channel que proporcionan un mecanismo de bucle invertido. Los extremos remotos de estos dos canales están conectados para que cualquier dato escrito en el objeto SinkChannel pueda aparecer en el objeto SourceChannel.

Creemos una tubería y escribamos datos en la tubería:

//通过Pipe.open()方法打开管道
Pipe pipe = Pipe.open();

//要向管道写数据,需要访问sink通道
Pipe.SinkChannel sinkChannel = pipe.sink();

//通过调用SinkChannel的write()方法,将数据写入SinkChannel
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
 sinkChannel.write(buf);
}

Mira cómo leer datos de la canalización:

Para leer los datos de la canalización, debe acceder al canal de origen:

Pipe.SourceChannel sourceChannel = pipe.source();

Llame al método read () del canal de origen para leer los datos:

ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = sourceChannel.read(buf);

El valor int devuelto por el método ead () nos dirá cuántos bytes se han leído en el búfer.

En este punto, hemos terminado el uso simple del canal. Si quieres usarlo, debes practicar más y usarlo en simulación, para que puedas saber cuándo y cómo usarlo. En la siguiente sección, hablar de selectores-selectores.

Algunas preguntas de entrevistas de alta frecuencia recopiladas en el último 2020 (todas organizadas en documentos), hay muchos productos secos, incluidos mysql, netty, spring, thread, spring cloud, jvm, código fuente, algoritmo y otras explicaciones detalladas, así como planes de aprendizaje detallados, entrevistas Clasificación de preguntas, etc. Para aquellos que necesitan obtener estos contenidos, agregue Q como: 11604713672

Supongo que te gusta

Origin blog.csdn.net/weixin_51495453/article/details/113854862
Recomendado
Clasificación