Conocimiento profundo de NIO y Epoll

Conocimiento profundo de NIO y Epoll

Modelo IO

El modelo IO significa qué tipo de canal se utiliza para enviar y recibir datos. Java admite un total de 3 modos IO de programación de red: BIO y NIO, AIO.

BIO (Bloqueo de E / S)

Este es un modelo de bloqueo sincrónico y un cliente solo puede vincular y procesar un hilo. Inserte la descripción de la imagen aquí
Ejemplo de código:

package com.atzxm.bio;

/**
 * @author zhouximin
 * @create 2021-01-17-下午7:13
 */
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketServer {
    
    
    public static void main(String[] args) throws IOException {
    
    
        ServerSocket serverSocket = new ServerSocket(9000);
        while (true) {
    
    
            System.out.println("等待连接。。");
            //阻塞方法
            Socket clientSocket = serverSocket.accept();
            System.out.println("有客户端连接了。。");
            handler(clientSocket);

            /*new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        handler(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();*/
        }
    }

    private static void handler(Socket clientSocket) throws IOException {
    
    
        byte[] bytes = new byte[1024];
        System.out.println("准备read。。");
        //接收客户端的数据,阻塞方法,没有数据可读时就阻塞
        int read = clientSocket.getInputStream().read(bytes);
        System.out.println("read完毕。。");
        if (read != -1) {
    
    
            System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
        }
        clientSocket.getOutputStream().write("HelloClient".getBytes());
        clientSocket.getOutputStream().flush();
    }
}
  • Desventajas:
    • La operación de lectura en el código IO es una operación de bloqueo. Si la conexión no realiza operaciones de lectura y escritura de datos, bloqueará el hilo y desperdiciará recursos.
    • Si hay demasiados subprocesos, provocará demasiados subprocesos del servidor y demasiada presión, como problemas de C10K
  • Escenario de aplicación
    • El método BIO es adecuado para un número relativamente pequeño y fijo de conexiones Este método requiere mayores recursos del servidor, pero el programa es simple y fácil de entender.

NIO (E / S sin bloqueo)

Sin bloqueo sincrónico, el modo de implementación del servidor es que un hilo puede manejar múltiples solicitudes (conexiones), las solicitudes de conexión enviadas por el cliente se registrarán en el selector de multiplexor y el multiplexor sondea hasta que haya una solicitud de IO conectada Procesamiento, JDK1 .4 comenzó a introducirse.
Escenarios de aplicación: el
método NIO es adecuado para arquitecturas con una gran cantidad de conexiones y conexiones relativamente cortas (operación ligera), como servidores de chat, sistemas de barrera, comunicación entre servidores y programación más compleja.
Ejemplos de código sin bloqueo NIO:

package com.atzxm.nio;


import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class NioServer {
    
    

    // 保存客户端连接
    static List<SocketChannel> channelList = new ArrayList<>();

    public static void main(String[] args) throws IOException, InterruptedException {
    
    

        // 创建NIO ServerSocketChannel,与BIO的serverSocket类似
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(9000));
        // 设置ServerSocketChannel为非阻塞
        serverSocket.configureBlocking(false);
        System.out.println("服务启动成功");

        while (true) {
    
    
            // 非阻塞模式accept方法不会阻塞,否则会阻塞
            // NIO的非阻塞是由操作系统内部实现的,底层调用了linux内核的accept函数
            SocketChannel socketChannel = serverSocket.accept();
            if (socketChannel != null) {
    
     // 如果有客户端进行连接
                System.out.println("连接成功");
                // 设置SocketChannel为非阻塞
                socketChannel.configureBlocking(false);
                // 保存客户端连接在List中
                channelList.add(socketChannel);
            }
            // 遍历连接进行数据读取
            Iterator<SocketChannel> iterator = channelList.iterator();
            while (iterator.hasNext()) {
    
    
                SocketChannel sc = iterator.next();
                ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                // 非阻塞模式read方法不会阻塞,否则会阻塞
                int len = sc.read(byteBuffer);
                // 如果有数据,把数据打印出来
                if (len > 0) {
    
    
                    System.out.println("接收到消息:" + new String(byteBuffer.array()));
                } else if (len == -1) {
    
     // 如果客户端断开,把socket从集合中去掉
                    iterator.remove();
                    System.out.println("客户端断开连接");
                }
            }
        }
    }
}
  • Guarde todos los canales obtenidos a través de serverSocket.accept () a través de una channelList. Luego recorrerá toda la channelList para intentar leer los datos. Sin embargo, algunos acaban de crear una conexión sin enviar datos, por lo que habrá muchos canales no válidos.
  • Resumen: Si hay demasiadas conexiones, habrá una gran cantidad de recorridos no válidos. Si hay 10,000 conexiones, solo 1,000 de ellas tienen datos de escritura, pero como las otras 9,000 conexiones no están desconectadas, aún tenemos que sondear y recorrer cada vez Diez mil veces, nueve de cada diez recorridos son inválidos, lo que obviamente no es un estado satisfactorio.

NIO presenta ejemplos de código de multiplexor:

package com.atzxm.nio;


import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NioSelectorServer {
    
    

    public static void main(String[] args) throws IOException, InterruptedException {
    
    

        // 创建NIO ServerSocketChannel
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        //ServerSocketChannel serverSocket2 = ServerSocketChannel.open();
        //绑定端口
        serverSocket.socket().bind(new InetSocketAddress(9000));
        //serverSocket2.socket().bind(new InetSocketAddress(9001));
        // 设置ServerSocketChannel为非阻塞
        serverSocket.configureBlocking(false);
        //serverSocket2.configureBlocking(false);
        // 打开Selector处理Channel,即创建epoll
        Selector selector = Selector.open();
        // 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣并且会生成
        //一个SelectionKey
        SelectionKey register = serverSocket.register(selector, SelectionKey.OP_ACCEPT);
        //serverSocket2.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务启动成功");

        while (true) {
    
    
            // 阻塞等待需要处理的事件发生
            selector.select();

            // 获取selector中注册的全部事件发生的 SelectionKey 实例
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            // 遍历SelectionKey对事件进行处理
            while (iterator.hasNext()) {
    
    
                SelectionKey key = iterator.next();
                // 如果是OP_ACCEPT事件,则进行连接获取和事件注册
                if (key.isAcceptable()) {
    
    
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    //生成一个SocketChannel 并且将其注册到selector中。
                    SocketChannel socketChannel = server.accept();
                    socketChannel.configureBlocking(false);
                    // 这里只注册了读事件,如果需要给客户端发送数据可以注册写事件
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("客户端连接成功");
                } else if (key.isReadable()) {
    
      // 如果是OP_READ事件,则进行读取和打印
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                    int len = socketChannel.read(byteBuffer);
                    // 如果有数据,把数据打印出来
                    if (len > 0) {
    
    
                        System.out.println("接收到消息:" + new String(byteBuffer.array()));
                    } else if (len == -1) {
    
     // 如果客户端断开连接,关闭Socket
                        System.out.println("客户端断开连接");
                        socketChannel.close();
                    }
                }
                //从事件集合里删除本次处理的key,防止下次select重复处理
                iterator.remove();
            }
        }
    }
}



Inserte la descripción de la imagen aquí

  • Para solucionar las deficiencias del Nio ordinario, se introduce un multiplexor (Selector), que
    luego de agregar un multiplexor, cada vez solo responderá al socketChannel donde ocurre el evento. Nota: Debido a que solo registramos un selector de aceptación, solo podemos responder a un selector de aceptación cada vez que seleccionamos.
  • ¿Cómo comprobar si el socketChannel registrado responde a los eventos?
    Antes de jdk1.4, las funciones de Linux select y poll se usaban para recorrer todos los sockchannels para ver si estaban respondiendo a eventos.
    Después de 1.4, use el modelo epoll.
    Análisis de código fuente de varias funciones clave
  1. Selector.open () // Crea un multiplexor y
    rastrea el código fuente hasta
    • int epfd = epoll_create (256)
      llamará a la función epoll_create () de Linux, que devolverá un descriptor de archivo. El archivo epoll correspondiente se puede encontrar a través de epfd.
  2. socketChannel.register (selector, SelectionKey.OP_READ)
    rastrea el código fuente hasta
    • pollWrapper.add (fd)
      fd es registrar el archivo correspondiente a socketChannel y ponerlo en una colección en pollWrapper, que está temporalmente asociado con el selector
  3. selector.select () // Bloquea la espera de eventos que necesitan ser procesados,
    rastrea el código fuente hasta
    • pollWrapper.poll (tiempo de espera);
    • updateRegistrations ();
      • epollCtl ()
        llamará a epoll_ctl () de Linux:
      int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
      
      Utilice la instancia de epoll a la que hace referencia el descriptor de archivo epfd para realizar una operación de operación en el descriptor de archivo de destino fd.
      El parámetro epfd representa el descriptor de archivo correspondiente a epoll y el parámetro fd representa el descriptor de archivo correspondiente al socket.
      El parámetro op tiene los siguientes valores:
      EPOLL_CTL_ADD: registra un nuevo fd en la dfpe y asocia el evento del evento;
      EPOLL_CTL_MOD: modifica el evento de escucha del fd registrado;
      EPOLL_CTL_DEL: elimina el fd de la dfpe e ignora el evento vinculado, en este momento el evento puede ser nulo; el
      evento de parámetro es una estructura
    struct epoll_event {
    
    
	    __uint32_t   events;      /* Epoll events */
	    epoll_data_t data;        /* User data variable */
	};
	
	typedef union epoll_data {
    
    
	    void        *ptr;
	    int          fd;
	    __uint32_t   u32;
	    __uint64_t   u64;
	} epoll_data_t;

Hay muchos valores opcionales para los eventos, y aquí están solo los más comunes:
EPOLLIN: indica que el descriptor de archivo correspondiente es legible;
EPOLLOUT: indica que el descriptor de archivo correspondiente es modificable;
EPOLLERR: indica el descriptor de archivo correspondiente Un error ha ocurrido;
0 si tiene éxito, -1 si falla

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

Espere el evento en el descriptor de archivo epfd. En epoll, hay una respuesta de evento en rdlist, si hay una respuesta de evento, se colocará en las teclas del selector, si no está bloqueado.
epfd es el descriptor de archivo correspondiente a Epoll, eventos representa el conjunto de todos los eventos disponibles de la persona que llama, maxevents representa el número máximo de eventos que se devolverán y el tiempo de espera es el período de tiempo de espera.

La capa inferior de multiplexación de E / S se implementa principalmente mediante funciones del kernel de Linux (select, poll, epoll). Windows no es compatible con epoll. La capa inferior de Windows se basa en la función de selección de winsock2 (no de código abierto)

Supongo que te gusta

Origin blog.csdn.net/null_zhouximin/article/details/112756039
Recomendado
Clasificación