NIO简单介绍和运用

  • NIO简单介
    NIO 非阻塞IO模型,基于缓冲区(Buffer)读写数据,读写后的数据通过通道(Channel)进行传输,采用选择器(Selector)管理多个通道从而实现高并发。
    核心组件:1. Buffer 为一个内存数组作为数据容器,代替传统的InputStream和OutputStream;2. Channel 双向数据传输;3. Selector 监听多通道区别于传统的单线程管理多连接
  • 传统的IO模型
    阻塞、非阻塞、多路复用、信号驱动、异步IO
  • 简单的NIO服务器接收客户端TCP请求
java 复制代码
 public void start(int port) throws IOException {
        //创建一个选择器管理通道
        Selector selector = Selector.open();
        //创建通道并注册到选择器(可创建多个通道)
        createChannel(selector, port);

        while (true) {
            //阻塞,等到有事件发生时继续执行
            selector.select();
            //处理事件
            Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
            while (keys.hasNext()) {
                SelectionKey key = keys.next();
                keys.remove();

                //新连接到达
                if (key.isAcceptable()) {
                    ServerSocketChannel socketChannel = (ServerSocketChannel) key.channel();
                    SocketChannel channel = socketChannel.accept();
                    channel.configureBlocking(false);
                    //注册新的读取事件
                    channel.register(selector, SelectionKey.OP_READ);
                    continue;
                }
                //可读取数据
                if (key.isReadable()) {
                    SocketChannel channel = (SocketChannel) key.channel();
                    //定义读取数据的缓冲区
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    //读取数据到缓冲
                    int bytes = channel.read(buffer);
                    //客户端关闭连接
                    if (bytes == -1) {
                        channel.close();
                        continue;
                    }
                    //切换为读取模式
                    buffer.flip();
                    String message = new String(buffer.array(), 0, bytes);
                    System.out.println("接收到消息");
                    //回显消息给客户端
                    channel.write(ByteBuffer.wrap(message.getBytes()));
                }
            }
        }
    }
java 复制代码
private void createChannel(Selector selector, int port) throws IOException {
        //创建通道并绑定端口
        ServerSocketChannel socketChannel = ServerSocketChannel.open();
        socketChannel.bind(new InetSocketAddress(port));
        //设置为非阻塞
        socketChannel.configureBlocking(false);
        //将通道注册到选择器
        socketChannel.register(selector, SelectionKey.OP_ACCEPT);
    }
  • Netty基于NIO的异步事件驱动的网络应用框架
    特性:异步和事件驱动、高性能(高效的线程模型和缓冲区机制)、易使用和灵活可扩展
    涉及到 channel 、callback(事件发生时的回调)、Future和Promise(异步计算)、EventLoop(事件循环)、Pipeline和ChannelHandler管道和处理器、bootstrap引导类
java 复制代码
 public static void start(int port) {
        // 创建两个EventLoopGroup事件循环组,一个用于接收客户端连接,一个用于处理客户端数据
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            start(bossGroup, workerGroup, port);
        } catch (Exception e) {
            log.warn("Netty服务【{}】发生异常", port, e);
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
java 复制代码
private static void start(EventLoopGroup bossGroup, EventLoopGroup workerGroup, int port) throws InterruptedException {
        //创建服务器启动对象
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        //parentGroup:这个事件组用于接收客户端的连接请求。它会创建一个ServerChannel,用于监听并接收客户端的连接。一旦有连接请求到达,parentGroup就会将连接分配给childGroup中的一个EventLoop来处理。
        //childGroup:这个事件组用于处理接收到的连接的数据。它会创建一个或多个Channel,用于与客户端进行通信。每个Channel都有一个关联的EventLoop,用于处理该Channel的I/O操作。childGroup会负责管理这些Channel和EventLoop,包括事件循环、线程分配、I/O操作等。
        //通过使用两个不同的事件组,可以实现多线程处理客户端连接和数据的模型。parentGroup负责接收连接请求并将其分配给childGroup处理,childGroup负责处理具体的连接和数据操作,从而提高服务器的并发性能。
        serverBootstrap.group(bossGroup, workerGroup)
                //.option(ChannelOption)可设置其它各种参数
                //服务器通道实现采用NioServerSocketChannel
                .channel(NioServerSocketChannel.class)
                //针对服务端的所有连接(不论已连接或是创建连接)
                .handler(new LoggingHandler())
                //针对已接收到的连接的处理器配置
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) {
                        //添加自定义解码器,如继承ByteToMessageCodec用于处理各种业务逻辑,解码器按添加的先后顺寻执行
                        //ByteToMessageCodec主要用于解码字节数据,将其转换为消息对象,常用于解决粘包和拆包问题
                        //ChannelInboundHandlerAdapter主要用于处理各种事件,如接收到数据、连接建立、异常处理等,同时为响应请求数据可以用用channel的writeAndFlush将数据写入并刷新网路
                        socketChannel.pipeline().addLast(new IotDataDecoder(), new IotBusinessDecoder());
                    }
                });
        //以下两行代码用于确保服务器在绑定端口后一直保持运行状态,直到接收到关闭信号
        //绑定服务器端口并启动服务 sync()使当前线程阻塞直到绑定服务器,绑定多个端口直接重复以下两行代码
        ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
        //等待服务器关闭
        channelFuture.channel().closeFuture().sync();
    }
  • 通过NIO将数据流写入到本地文件
java 复制代码
 private void write(InputStream stream, String targetPath) throws IOException {
        //创建通道
        FileChannel outChannel = FileChannel.open(Paths.get(targetPath), StandardOpenOption.CREATE, StandardOpenOption.WRITE);
        ReadableByteChannel inChannel = Channels.newChannel(stream);
        //创建缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(2048);
        //写入和读取
        while (inChannel.read(buffer) != -1) {
            //调整缓冲区的位置以便读取,将位置调整到0
            buffer.flip();
            outChannel.write(buffer);
            //将缓冲区未读取的位置移动到起始位置
            buffer.compact();
        }
        buffer.flip();
        //判断是否有剩余内容
        while (buffer.hasRemaining()) {
            outChannel.write(buffer);
        }

    }