NIO模型

335 阅读8分钟

NIO模型

1. 简介

Java NIO是从JDK1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓存区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。

两者的区别

BIO NIO
面向流(Stream Oriented) 面向缓冲区(Buffer Oriented)
阻塞IO(Blocking IO) 非阻塞IO(Non Blocking IO)
选择器(Selectors)

2. 工作流程

2.1 通道和缓冲区

通道表示打开到IO设备的连接,不存储数据。

缓冲区,对数据进行主要的处理

2.2 使用流程

**思想:**使用前,获取用于连接IO设备的通道以及用于容纳数据的缓冲区。

相较于传统BIO,NIO使用了双向通道,通道不用来传输任何数据,数据通过Buffer的移动来进行IO操作。

2.3 底层原理

NIO中引入了四个概念。

capacity缓冲区的容量,表示用来进行IO的最大数据量

limit第一个不应该进行写入或者读取的索引

position当前索引的位置,相当于一个读或者写的指针

mark和reset指向一个索引。可以通过remark`回到这个索引处

Buffer的底层是一个对应类型的数组

position<limit<capacity 其中的mark必须在0和limit之间

Java.nio包下提供了除了boolean类型的缓冲区

 ByteBuffer b ; 
    ShortBuffer s ;
    IntBuffer i1 ;
    LongBuffer l;
    FloatBuffer floatBuffer ;
    DoubleBuffer doubleBuffer ;
    CharBuffer charBuffer ;

2.4 直接缓冲区和非直接缓冲区

直接缓冲区是直接创建在物理内存中的。通过allocate方法分配

非直接缓冲区是创建在堆中的。通过allocateDirect()方法分配

非直接缓冲区的工作流程:应用程序读取数据的时候,首先需要将数据加载到内核地址空间,然后再将数据复制到用户地址空间中,这样应用程序才可以读取。

直接缓冲区工作流程:应用程序读取数据的时候,直接在物理内存中读取,抛弃了复制的过程

2.5 通道的执行流程

将数据加载到内存中,然后IO接口创建一个通道,用来具体的数据读取

3. 简单的API

通道API

FileChannel  用于读取、写入、映射和操作文件的通道
DatagramChannel  通过UDP读写网络中的数据通道
SocketChannel   通过TCP读写网络中的数据
ServerSocketChannel 监听新进来的TCP连接,对于每一个进来的连接都会创建一个SocketChannel    

获取通道:

输入流的getChannel()方法
FileChannel的open方法
Files工具类中的newByteChannel

常见API用法

  • 字符的解码和加码,通过CharSet实现
  • 直接缓冲区的IO操作。
    • 通过BufferallocateDirect分配
    • 通过FileChannelopen方法创建
//打开文件,按照只读模式
        try {
            //指定了通道的模式,读取数据
            FileChannel in = FileChannel.open(Paths.get("G:\\BugReport.txt"), StandardOpenOption.READ);
            //写数据,读数据,创建文件
            FileChannel out = FileChannel.open(Paths.get("G:\\BugReport3.txt"),
                    StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
            //获取物理映射空间
            MappedByteBuffer inMap = in.map(FileChannel.MapMode.READ_ONLY, 0, in.size());
            MappedByteBuffer outMap = out.map(FileChannel.MapMode.READ_WRITE, 0, in.size());

            //复制文件,先直接在物理空间中获取,然后再进行写入
            byte[] bytes = new byte[inMap.limit()];
            inMap.get(bytes);
            outMap.put(bytes);

            System.out.println();

        } catch (IOException e) {
            e.printStackTrace();
        }

4. 网络非阻塞IO通信

对于传统的IO来说,都是阻塞的,即线程会一直等待数据,直到数据被读取或者写入。此时会造成大量的CPU资源浪费

而NIO中添加了一个选择器,他可以选择一个数据以及填充完成的通道发送给服务端执行。即当数据没有来之前,线程可以执行其他的事情.

非阻塞IO通信图

客户端:

public static void main(String[] args) throws IOException {
        //1. 获取通道
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1"10001));

        //2. 切换非阻塞模式
        sChannel.configureBlocking(false);

        //3. 分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);

        //4. 发送数据给服务端
        Scanner scan = new Scanner(System.in);

        while(scan.hasNext()){
            String str = scan.next();
            buf.put((new Date().toString() + "\n" + str).getBytes());
            buf.flip();
            sChannel.write(buf);
            buf.clear();
        }

        //5. 关闭通道
        sChannel.close();
    }

服务端:

public static void main(String[] args) throws IOException {
        //1. 获取通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();

        //2. 切换非阻塞模式
        ssChannel.configureBlocking(false);

        //3. 绑定连接
        ssChannel.bind(new InetSocketAddress(10001));

        //4. 获取选择器
        Selector selector = Selector.open();

        //5. 将通道注册到选择器上, 并且指定“监听接收事件”
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);

        //6. 轮询式的获取选择器上已经“准备就绪”的事件
        while(selector.select() > 0){

            //7. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();

            while(it.hasNext()){
                //8. 获取准备“就绪”的是事件
                SelectionKey sk = it.next();

                //9. 判断具体是什么事件准备就绪
                if(sk.isAcceptable()){
                    //10. 若“接收就绪”,获取客户端连接
                    SocketChannel sChannel = ssChannel.accept();

                    //11. 切换非阻塞模式
                    sChannel.configureBlocking(false);

                    //12. 将该通道注册到选择器上
                    sChannel.register(selector, SelectionKey.OP_READ);
                }else if(sk.isReadable()){
                    //13. 获取当前选择器上“读就绪”状态的通道
                    SocketChannel sChannel = (SocketChannel) sk.channel();

                    //14. 读取数据
                    ByteBuffer buf = ByteBuffer.allocate(1024);

                    int len = 0;
                    while((len = sChannel.read(buf)) > 0 ){
                        buf.flip();
                        System.out.println(new String(buf.array(), 0, len));
                        buf.clear();
                    }
                }

                //15. 取消选择键 SelectionKey
                it.remove();
            }
        }


    }

本文源码已经收录到开源项目MakerStack
Github地址

本文使用 mdnice 排版