美文网首页
NIO的使用介绍

NIO的使用介绍

作者: 文景大大 | 来源:发表于2020-04-15 20:54 被阅读0次

一、NIO简介

BIO是基于字节流和字符流进行操作的,而NIO是基于通道和缓冲区进行操作的,数据总是从通道中读取到缓冲区,或者从缓冲区写入到通道中。

BIO是阻塞式的,而NIO是非阻塞式的,读写的效率要更加地高。

二、Channel

Channel有点类似BIO中的流的概念,但是比流要强大地多,主要体现在以下方面:

  • 可以支持数据双向流动;
  • 可以异步地读写;
  • 总是和Buffer进行数据交互;

Channel有以下几个基本类型:

  • FileChannel,用于文件IO,无法设置为非阻塞方式;
  • DatagramChannel,用于UDP的网络IO;
  • SocketChannel,用于TCP的网络IO;
  • ServerSocketChannel,用于监听TCP的网络IO,并创建SocketChannel;

关于Channel的使用,给出如下一个简单的示例,将test1中的内容使用channel和buffer拷贝到test2文件中:

    public static void main(String[] args) throws IOException {
        RandomAccessFile raf1 = new RandomAccessFile("test1.txt", "rw");
        RandomAccessFile raf2 = new RandomAccessFile("test2.txt", "rw");
        FileChannel fc1 = raf1.getChannel();
        FileChannel fc2 = raf2.getChannel();

        ByteBuffer bb = ByteBuffer.allocate(100);

        // 将文件内容通过channel读到buffer中
        log.info("读取字节个数为:{}", fc1.read(bb));

        // 反转buffer的状态,由写状态变为读状态
        bb.flip();

        // 从buffer中将内容读出到channel中
        fc2.write(bb);

        // buffer中的数据已经写出,但是本身内容还存在,需要清空
        bb.clear();
        fc1.close();
        fc2.close();
        raf1.close();
        raf2.close();
    }

除此之外,我们还可以直接将数据从一个channer传输到另外一个channel中。

三、Buffer

buffer本质上是一块可以读写数据的缓存,一般遵循以下的使用步骤:

  • 写入数据到buffer;
  • 调用flip方法切换到读模式;
  • 从buffer中读取数据;
  • 调用clear方法清空整个缓冲区,或者调用compact方法清除已经都去过的数据;

在buffer中有如下几个重要的属性需要掌握:

  • capacity,标识缓冲区buffer的字节总大小量;
  • position,在写模式下,初始值为0,写完后,切换为读模式,则该值被重置为0;
  • limit,在写模式下,等同于capacity,在读模式下,代表已经写入字节数的总量;

Buffer基本类型:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShrotBuffer

这些类型基本上涵盖了你能通过IO发送的数据类型。

channel和buffer之间的读写关系并不是严格的一对一关系,可以通过scatter方式将一个channel中的数据写入多个buffer之中,也可以通过gather方式将多个buffer中的数据读取到一个channel中。

四、Selector

一个Selector线程能注册多个Channel,并对它们进行(非)阻塞式地监听,任意一个Channel有事件就绪,Selector就将该事件交给对应的业务线程进行处理,Selector继续对注册的Channel进行监听。

我们这里以服务端的一个实现来介绍Selector多路复用器的使用:

@Slf4j
public class Test001 {
    private static final Boolean RUNNING_FLAG = Boolean.TRUE;
    private static Integer connectCount = 0;
    private static ByteBuffer buffer = ByteBuffer.allocate(1024);
    private static byte[] bytes = new byte[1024];

    public static void main(String[] args) throws IOException {
        // 创建Selector
        Selector selector = Selector.open();
        // 创建channnel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // channel设置非阻塞方式
        serverSocketChannel.configureBlocking(false);
        // 绑定监听端口
        serverSocketChannel.socket().bind(new InetSocketAddress(8000));
        // channel注册到selector中,并设置为连接接收模式
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        // selector会一直循环获取就绪状态的channel
        while(RUNNING_FLAG){
            // 获取所有就绪状态的channel
            selector.select();
            Set<SelectionKey> selectionKeys = selector.selectedKeys();

            // 循环处理所有就绪的channel
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while(iterator.hasNext()){
                SelectionKey currentKey = iterator.next();
                // 当前正在处理的channel需要移除,避免再次处理
                iterator.remove();
                // 业务逻辑,对channel进行处理
                handleServerSocketChannel(currentKey, selector);
            }
        }

        // 需要结束程序时,关闭selector即可,其上注册的所有channel资源会自动关闭
        selector.close();
    }

    private static void handleServerSocketChannel(SelectionKey key, Selector selector) throws IOException {
        if (key.isAcceptable()){
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel)key.channel();
            // 为新的接入请求创建一个channel
            SocketChannel socketChannel = serverSocketChannel.accept();
            // 同样设置非阻塞方式
            socketChannel.configureBlocking(false);
            // channel注册到selector中,并设置为可读模式
            SelectionKey selectionKey = socketChannel.register(selector, SelectionKey.OP_READ);
            // 设置当前连接请求channel的编号
            selectionKey.attach(++connectCount);
        } else if(key.isReadable()){
            SocketChannel socketChannel = (SocketChannel)key.channel();
            socketChannel.read(buffer);
            // 缓冲区由写模式切换为读模式
            buffer.flip();
            // 获取客户端发送的内容
            buffer.get(bytes);
            log.info("客户端发送的消息为:{}", new String(bytes, "UTF-8"));
            buffer.clear();
            // 缓冲区由读模式切换为写模式
            buffer.flip();
            // 将需要返回给客户端的内容写入buffer
            buffer.put("服务端成功接收!".getBytes());
            socketChannel.write(buffer);
        }
    }
}

五、总结

在实际的开发实践中,很少有人直接使用NIO,因为它使用起来比较复杂,而且在linux上还存在Bug,比如epoll的空轮询导致CPU100%的问题;而且重复造轮子会导致开发效率底下,无法保证程序的质量,所以大家都倾向于使用基于NIO实现的中间件工具,比如Netty。

六、参考文献

相关文章

网友评论

      本文标题:NIO的使用介绍

      本文链接:https://www.haomeiwen.com/subject/tpwgmhtx.html