关于java:一文搞懂NIO

6次阅读

共计 9819 个字符,预计需要花费 25 分钟才能阅读完成。

前言

后面说过 Java 中的 IO 操作,然而传统的 IO 是阻塞模式的,在高并发的零碎中必定是不可行的,这次咱们来介绍 Java 中提供的另外一种 IO 操作 –NIO

首先,咱们要晓得应用程序中的 IO 和操作系统的 IO 是有区别的,应用程序的 IO 最终都须要依附操作系统的 IO 来实现最终的操作。这时候就须要抉择适合的 IO 模型,常见的 IO 模型有四种:

  1. 同步阻塞 IO(Blocking IO)

    阻塞 IO 指的是须要内核 IO 操作彻底实现后,能力继续执行上面的操作。Java 中传统的 IO 和 socket 默认都是同步阻塞 IO

  2. 同步非阻塞 IO(Non-blocking IO)

    非阻塞 IO 指的是不须要期待内核 IO 执行完,能够立刻返回用户空间,持续上面的操作,此时内核会给用户一个状态值。很多人认为 Java 中的 NIO 就是 Non-blocking IO 的缩写,也就是同步非阻塞 IO,其实不是的,Java 中 NIO 是 new IO,指的是上面 IO 多路复用模型。

  3. IO 多路复用

    IO 多路复用指的是一个线程能够监督多个文件描述符,一旦某个文件描述符就绪(可读 / 可写),内核就会将就绪状态返回给应用程序,应用程序依据就绪状态,进行相应的 IO 操作。

  4. 异步 IO

    异步 IO 相似于回调模式,用户空间向内核空间注册了各种 IO 事件的回调函数,由内核去被动调用。

四种 IO 模式总结:

同步阻塞 IO 模型是 Java 中传统 IO 默认应用的,个别零碎并发不高,应用此模型还是能够的;同步非阻塞 IO 模型听起来由原来的同步转成了异步,然而此模型须要用户线程始终去询问内核是否就绪,这就会占用大量的 CPU 工夫,在高并发的状况下此模型显然是不可用的;IO 多路复用模型,是当初支流的高并发下的 IO 模型;异步 IO 模型,实践上是性能最高的一种 IO 模型,然而很多操作系统底层还不欠缺,因而在性能上没有显著的劣势。

Java NIO

当初很多支流的框架和中间件都采纳了 Java 的 NIO,比方 tomcat、Netty 等。下面说到,Java 中的 NIO 采纳的是 IO 多路复用模型,此模型就是经典的 Reactor 反应器模式

Java NIO 由上面三个外围组件组成:

  • Channel(通道)
  • Buffer(缓冲区)
  • Selector(选择器)

Channel(通道)

在传统的 IO 中,所有的 IO 操作都须要通过输出流和输入流来实现;然而在 NIO 中,所有的 IO 操作都是从通道开始的,一个通道既能够输出也能够输入。

Buffer(缓冲区)

应用程序和通道交互就须要通过缓冲区,通道的读取就是将数据从通道写入到缓冲区,通道的写入就是将数据从缓冲区写入到通道中。

Selector(选择器)

Java 中的 NIO 是一种 IO 多路复用模型,那它是通过什么实现的呢?这就须要依附它的第三个组件 –Selector 选择器。通过选择器,一个线程能够查问多个通道的 IO 事件的就绪状态。

组件详解

Buffer 缓冲区

Buffer 类是一个抽象类,在 NIO 中有 8 种缓冲区类,别离如下:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer、MappedBuffer。其中应用最广的是 ByteBuffer。

Buffer 的重要属性

  1. capacity(容量)

    初始化 Buffer 时,须要指定缓冲区的容量,当写入的数据超过这个容量时就不能再写入了。缓冲区的容量一旦初始化,就不能再扭转了,这是因为 Buffer 实质上就是一个内存块,相当于一个数组,内存调配好当前,它的大小就不能扭转了。capacity 容量不是 byte[]数组的字节数量,而是写入的数据对象的数量。

  2. position(读写地位)

    在写模式下:刚进入写模式时,position 为 0,示意从头开始写,每写入一个数据,position 地位都往后移一位,当 position 达到 limit- 1 的时候,就不能再写入了。

    在读模式下:刚进入读模式时,position 会重置为 0,每读取一个数据时,position 地位都往后移一位,当 position 达到 limit- 1 的时候,就没有数据可读了。

  3. limit(读写的限度)

    在写模式下,limit 示意最大下限,等于 capacity 值。当切换到读模式时,limit 会变成切换前写模式时的 position 地位,而 position 则重置为 0。

Buffer 的重要办法

  1. allocate() 创立缓存区并初始化
  2. put() 写入数据
  3. flip() 翻转(进入读模式)
  4. get() 获取数据
  5. rewind() 倒带(反复读)
  6. mark()/reset() 标记 / 重置(从地位从新读取)
  7. clear() 革除(进入写模式)
public class NioDemo {public void test() {IntBuffer intBuffer = IntBuffer.allocate(10);
        print("初始化", intBuffer);
        for (int i = 0; i < 6; i++) {intBuffer.put(i);
        }
        print("写入 6 个数据", intBuffer);
        intBuffer.flip();
        print("翻转后进入读模式", intBuffer);
        for (int i = 0; i < 2; i++) {intBuffer.get();
        }
        print("读取 2 个数据", intBuffer);
        intBuffer.rewind();
        print("倒带", intBuffer);
        for (int i = 0; i < 6; i++) {if (i == 3) {intBuffer.mark();
            }
            intBuffer.get();}
        print("读取 3 个数据,并保留第四个地位", intBuffer);
        intBuffer.reset();
        for (int i = 3; i < 6; i++) {intBuffer.get();
        }
        print("reset 后读取数据", intBuffer);
        intBuffer.clear();
        print("clear 后进入写模式", intBuffer);
    }

    private void print(String name,IntBuffer intBuffer) {System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>" + name);
        System.out.println(intBuffer.toString());
    }

    public static void main(String[] args) {NioDemo demo = new NioDemo();
        demo.test();}
}

// 输入
>>>>>>>>>>>>>>>>>>>>>>>>> 初始化
java.nio.HeapIntBuffer[pos=0 lim=10 cap=10]
>>>>>>>>>>>>>>>>>>>>>>>>> 写入 6 个数据
java.nio.HeapIntBuffer[pos=6 lim=10 cap=10]
>>>>>>>>>>>>>>>>>>>>>>>>> 翻转
java.nio.HeapIntBuffer[pos=0 lim=6 cap=10]
>>>>>>>>>>>>>>>>>>>>>>>>> 读取 2 个数据
java.nio.HeapIntBuffer[pos=2 lim=6 cap=10]
>>>>>>>>>>>>>>>>>>>>>>>>> 倒带
java.nio.HeapIntBuffer[pos=0 lim=6 cap=10]
>>>>>>>>>>>>>>>>>>>>>>>>> 读取 3 个数据,并保留第四个地位
java.nio.HeapIntBuffer[pos=6 lim=6 cap=10]
>>>>>>>>>>>>>>>>>>>>>>>>>reset 后读取数据
java.nio.HeapIntBuffer[pos=6 lim=6 cap=10]
>>>>>>>>>>>>>>>>>>>>>>>>>clear 后进入写模式
java.nio.HeapIntBuffer[pos=0 lim=10 cap=10]

Channel 通道

Java NIO 中有四种常见的 Channel:FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel。

  1. FileChannel:文件通道,用于文件的读写。
  2. SocketChannel:套接字通道,用于 TCP 连贯的数据读写。
  3. ServerSocketChannel:服务端套接字通道,用于监听 TCP 连贯。
  4. DatagramChannel:数据报通道,用于 UDP 连贯的数据读写。

FileChannel 文件通道

FileChannel 为阻塞模式,不能设置为非阻塞模式。

上面通过一个简略的复制文件来介绍 FileChannel 的用法

public class ChannelDemo {

    /**
     * 获取输出通道
     * @param filePath
     * @return
     * @throws Exception
     */
    public FileChannel getInputChannel(String filePath) throws Exception {FileInputStream inputStream = new FileInputStream(filePath);
        return inputStream.getChannel();}

    /**
     * 获取输入通道
     * @param targetPath
     * @return
     * @throws Exception
     */
    public FileChannel getOutputChannel(String targetPath) throws Exception {FileOutputStream outputStream = new FileOutputStream(targetPath);
        return outputStream.getChannel();}

    /**
     * 数据从输出通道读取到缓冲区,再从缓冲区读取到输入通道
     * @param inputChannel
     * @param outputChannle
     * @throws Exception
     */
    public void copyFile(FileChannel inputChannel,FileChannel outputChannle) throws Exception {ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        // 初始是写入模式
        while (inputChannel.read(byteBuffer) != -1) {System.out.println("byteBuffer 写入了"+byteBuffer.position()+"个数据");
            // 变成读取模式
            byteBuffer.flip();
            while (outputChannle.write(byteBuffer) != 0) {System.out.println("byteBuffer 读取到 outputChannle 实现");
            }
            // 读取完转为写入模式
            byteBuffer.clear();}
        inputChannel.close();
        outputChannle.close();}

    public static void main(String[] args) throws Exception{ChannelDemo demo = new ChannelDemo();
        FileChannel inputChannel = demo.getInputChannel("c:/soft/test/hello.txt");
        FileChannel outputChannel = demo.getOutputChannel("c:/soft/test/hello-copy.txt");
        demo.copyFile(inputChannel, outputChannel);
    }
}

SocketChannel 套接字通道

socketChannel 负责 socket 传输,作用于客户端和服务端,反对阻塞和非阻塞模式,个别都是应用非阻塞模式,应用 socketChannel.configureBlocking(false) 设置为非阻塞模式。

上面通过一个简略的客户端发送 socket 连贯示例来介绍 SocketChannel

public class SocketClientDemo {public void send(byte[] bytes) throws Exception {// 通过 SocketChannel 的静态方法 open()获取 socketChannel 实例
        SocketChannel socketChannel = SocketChannel.open();
        // 连贯服务端的 ip 和端口
        socketChannel.socket().connect(new InetSocketAddress("127.0.0.1",1234));
        // 设置为非阻塞
        socketChannel.configureBlocking(false);
        // 因为非阻塞的,连贯会立刻返回,所以此处须要轮询,查看是否实现连贯
        while (!socketChannel.finishConnect()) {System.out.println("还没连贯上,重试。。。");
        }
        System.out.println("连贯胜利!");
        // 应用缓存区发送数据
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        // 向缓冲区中写入数据
        byteBuffer.put(bytes);
        // 翻转,缓存区变成读取模式
        byteBuffer.flip();
        // 缓冲区中数据写入到 socketChannel 通道中
        socketChannel.write(byteBuffer);
        System.out.println("写入实现");
        // 敞开连贯,失常状况下在 finally 中敞开,这里只是简略演示
        socketChannel.close();}

    public static void main(String[] args) throws Exception {SocketClientDemo demo = new SocketClientDemo();
        demo.send("hello".getBytes());
    }
}

ServerSocketChannel 是服务端连贯 socket 数据的通道,具体用例联合前面介绍的选择器一起介绍。

DatagramChannel 数据报通道

DatagramChannel 通道用来解决 UDP 协定的,UDP 协定和 TCP 协定不一样,它间接通过 IP 和端口就能够间接向对方发送数据,不须要建设连贯。

上面通过简略的客户端用例来介绍 DatagramChannel 用法

public class DatagramClient {public void send(String data) throws Exception{DatagramChannel datagramChannel = DatagramChannel.open();
        datagramChannel.configureBlocking(false);
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        byteBuffer.put(data.getBytes());
        byteBuffer.flip();
        datagramChannel.send(byteBuffer, new InetSocketAddress("127.0.0.1", 4444));
        datagramChannel.close();}

    public static void main(String[] args) throws Exception{new DatagramClient().send("hello udp");
    }
}

DatagramChannel 服务端的用法也是通过上面的选择器一起介绍。

Selector 选择器

Selector 是第三个重要的组件,NIO 次要是通过这个组件来实现多路复用。一个通道代表一个连贯,通过选择器来同时监控多个通道的变动。通道首先通过 register 实现在选择器上的注册,注册时须要两个参数,一个是选择器实例,一个是要监控的 IO 事件类型,这个事件类型有上面四种:

  • SelectionKey.OP_READ 可读
  • SelectionKey.OP_WRITE 可写
  • SelectionKey.OP_CONNECT 连贯
  • SelectionKey.OP_ACCEPT 承受

上面通过简略的 DatagramChannel 服务端的代码来介绍 Selector 的应用

public class DatagramServer {public void accept() throws Exception {DatagramChannel datagramChannel = DatagramChannel.open();
        datagramChannel.configureBlocking(false);
        datagramChannel.bind(new InetSocketAddress(4444));
        // 通过 Selector 的 open()办法获取选择器实例
        Selector selector = Selector.open();
        // 通道注册到选择器上
        datagramChannel.register(selector, SelectionKey.OP_READ);
        System.out.println("开启监听");
        // 轮询
        while (selector.select() > 0) {
            // 获取选择器上所有变动的选择键
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            // 遍历
            while (iterator.hasNext()) {
                // 获取具体的选择键实例
                SelectionKey selectionKey = iterator.next();
                // 可读事件
                if (selectionKey.isReadable()) {
                    // 上面实现具体可读当前的解决
                    System.out.println("可读");
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    // 和 ServerSocketChannel 承受数据不一样,这里应用 receive
                    datagramChannel.receive(byteBuffer);
                    System.out.println(new String(byteBuffer.array(), 0, byteBuffer.limit()));
                }
            }
            // 移除此选择键,避免反复读取
            iterator.remove();}
        selector.close();
        datagramChannel.close();}

    public static void main(String[] args) throws Exception{new DatagramServer().accept();}
}

总结一下 Selector 选择器的应用步骤:首先获取选择器实例,而后将通道注册到选择器实例上,接着选出监听的 IO 事件,最初进行具体的解决逻辑。

上面介绍一下平时罕用的 Socket 服务端程序,看看通过 Selector 选择器怎么实现多路复用,也就是一个线程实现多个 IO 事件的处理程序。

public class SocketServerDemo {public void accept() throws Exception {
        // 获取选择器实例
        Selector selector = Selector.open();
        // 获取 ServerSocketChannel 实例
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        // 监听 5555 端口
        serverSocketChannel.bind(new InetSocketAddress(5555));
        // ServerSocketChannel 通道注册到选择器上,并监听可连贯的事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        // 轮询
        while (selector.select() > 0) {
            // 所有选择键
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                // 获取具体选择键实例
                SelectionKey selectionKey = iterator.next();
                // 如果有连贯事件,阐明有客户端通过此端口连贯到服务端
                if (selectionKey.isAcceptable()) {
                    // 进行可连贯的解决逻辑
                    handlerAccept(selectionKey);
                } else if (selectionKey.isReadable()) {handlerRead(selectionKey);
                }
            }
            // 移除,避免反复生产
            iterator.remove();}
        serverSocketChannel.close();}

    private void handlerAccept(SelectionKey selectionKey) throws IOException {System.out.println("解决连贯");
        // 获取 SocketChannel 实例,用来进去数据传输
        SocketChannel socketChannel = ((ServerSocketChannel) selectionKey.channel()).accept();
        // 设置为非阻塞模式
        socketChannel.configureBlocking(false);
        // 将 SocketChannel 通道注册到选择器上,并监听可读取事件
        socketChannel.register(selectionKey.selector(), SelectionKey.OP_READ);
    }

    private void handlerRead(SelectionKey selectionKey) throws Exception {System.out.println("解决数据");
        // 获取选择键上的 SocketChannel
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        // 设置为非阻塞模式
        socketChannel.configureBlocking(false);
        // 数据读取到缓冲区中
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        if (socketChannel.read(byteBuffer) == -1) {System.out.println("无数据,敞开");
            socketChannel.close();
            return;
        }
        String s = new String(byteBuffer.array()).trim();
        // 打印客户端传过来的数据
        System.out.println(s);
        System.out.println("读取实现");
    }

    public static void main(String[] args) throws Exception {new SocketServerDemo().accept();}
}

总结

Java 的 NIO 次要通过 Buffer、Channel 和 Selector 三个组件来实现多路复用的 IO 操作,首先将通道注册到选择器上,而后查问选择器上对应的选择键,获取监听的 IO 事件,接着将通道中数据读取或写入缓冲区中,实现对应的事件处理。

下面次要是介绍了 Java 中 NIO 的简略操作,外面还有很多待优化的中央,比方监听逻辑和读写逻辑都注册在同一个选择器上,如果读写耗时,还是会阻塞监听逻辑的。前面通过介绍 Reactor 模式来解决这些问题。


<center> 扫一扫,关注我 </center>

正文完
 0