IO-NIO

介绍

​ Java NIO(New IO)也有人称之为 java non-blocking IO,也有人称为是new io,是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。

​ NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。

流与块

I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。

面向流的I/O 一次处理一个字节数据:

一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。

面向块的I/O 一次处理一个数据块:

​ 按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。

I/O 包和 NIO 已经很好地集成了,有一些工具类可以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。

核心部分

NIO 有三大核心部分:Channel( 通道) ,Buffer( 缓冲区), Selector( 选择器)。

在 Java NIO 中的通道(Channel)就相当于操作系统的内核空间(kernel space)的缓冲区,而缓冲区(Buffer)对应的相当于操作系统的用户空间(user space)中的用户缓冲区(user buffer)。

Channel( 通道)

​ 通道,被建立的一个应用程序和操作系统交互事件、传递内容的渠道(注意是连接到操作系统)。一个通道会有一个专属的文件状态描述符。那么既然是和操作系统进行内容的传递,那么说明应用程序可以通过通道读取数据,也可以通过通道向操作系统写数据

通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。且通道是双向的,可以用于读、写或者同时用于读写。

通道包括以下类型:

  • FileChannel: 从文件中读写数据;
  • DatagramChannel: 通过 UDP 读写网络中数据;
  • SocketChannel: 通过 TCP 读写网络中数据;
  • ServerSocketChannel: 可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。

Buffer( 缓冲区)

读写都需要经过缓冲区,不会直接对通道进行读写数据,只能通过缓冲区对通道进行读写数据。缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。

缓冲区包括以下类型:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

缓冲区状态变量

  • capacity: 最大容量;
  • position: 当前已经读写的字节数;
  • limit: 还可以读写的字节数。

Selector( 选择器)

NIO 常常被叫做非阻塞 IO。NIO 实现了 IO 多路复用中的 Reactor 事件驱动模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。

​ 通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。

​ 注意:

只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。

FileChannel

FileChannel 是一个用于文件读写、映射和操作的通道,同时它在并发环境下是线程安全的,基于 FileInputStream、FileOutputStream 或者 RandomAccessFile 的 getChannel() 方法可以创建并打开一个文件通道。FileChannel 定义了 transferFrom() 和 transferTo() 两个抽象方法,它通过在通道和通道之间建立连接实现数据传输的。

NIO实现过程

1)创建选择器

Selector selector = Selector.open();

2)将通道注册到选择器上

ServerSocketChannel ssChannel =ServerSocketChannel.open(); ssChannel.configureBlocking(false); ssChannel.register(selector,SelectionKey.OP_ACCEPT);

通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了。在将通道注册到选择器上时,还需要指定要注册的具体事件。

3)监听事件

使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。

int num = selector.select();

4)获取到达的事件

Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if (key.isAcceptable()) {
        // ...
    } else if (key.isReadable()) {
        // ...
    }
    keyIterator.remove();
}

5)事件循环

因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。

while (true) {
    int num = selector.select();
    Set<SelectionKey> keys = selector.selectedKeys();
    Iterator<SelectionKey> keyIterator = keys.iterator();
    while (keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        if (key.isAcceptable()) {
            // ...
        } else if (key.isReadable()) {
            // ...
        }
        keyIterator.remove();
    }
}

套接字NIO 实例

public class NIOServer {

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

        Selector selector = Selector.open();

        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        ssChannel.configureBlocking(false);
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);

        ServerSocket serverSocket = ssChannel.socket();
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
        serverSocket.bind(address);

        while (true) {

            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = keys.iterator();

            while (keyIterator.hasNext()) {

                SelectionKey key = keyIterator.next();

                if (key.isAcceptable()) {

                    ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();

                    // 服务器会为每个新连接创建一个 SocketChannel
                    SocketChannel sChannel = ssChannel1.accept();
                    sChannel.configureBlocking(false);

                    // 这个新连接主要用于从客户端读取数据
                    sChannel.register(selector, SelectionKey.OP_READ);

                } else if (key.isReadable()) {

                    SocketChannel sChannel = (SocketChannel) key.channel();
                    System.out.println(readDataFromSocketChannel(sChannel));
                    sChannel.close();
                }

                keyIterator.remove();
            }
        }
    }

    private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        StringBuilder data = new StringBuilder();

        while (true) {

            buffer.clear();
            int n = sChannel.read(buffer);
            if (n == -1) {
                break;
            }
            buffer.flip();
            int limit = buffer.limit();
            char[] dst = new char[limit];
            for (int i = 0; i < limit; i++) {
                dst[i] = (char) buffer.get(i);
            }
            data.append(dst);
            buffer.clear();
        }
        return data.toString();
    }
}
public class NIOClient {

    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 8888);
        OutputStream out = socket.getOutputStream();
        String s = "hello world";
        out.write(s.getBytes());
        out.close();
    }
}

对比

  • NIO 是非阻塞的
  • NIO 面向块,I/O 面向流

典型的多路复用IO实现

​ 目前流程的多路复用IO实现主要包括四种: selectpollepollkqueue。下表是他们的一些重要特性的比较:

IO模型相对性能关键思路操作系统JAVA支持情况
select较高Reactorwindows/Linux支持,Reactor模式(反应器设计模式)。Linux操作系统的 kernels 2.4内核版本之前,默认使用select;而目前windows下对同步IO的支持,都是select模型
poll较高ReactorLinuxLinux下的JAVA NIO框架,Linux kernels 2.6内核版本之前使用poll进行支持。也是使用的Reactor模式
epollReactor/ProactorLinuxLinux kernels 2.6内核版本及以后使用epoll进行支持;Linux kernels 2.6内核版本之前使用poll进行支持;另外一定注意,由于Linux下没有Windows下的IOCP技术提供真正的 异步IO 支持,所以Linux下使用epoll模拟异步IO
kqueueProactorLinux目前JAVA的版本不支持

​ **多路复用IO技术最适用的是“高并发”场景,所谓高并发是指1毫秒内至少同时有上千个连接请求准备好。**另一方面,使用JAVA NIO进行功能实现,相对于传统的Socket套接字实现要复杂一些,所以实际应用中,需要根据自己的业务需求进行技术选择。

Reactor事件驱动模型

​ **Reactor模型是以事件驱动来处理网络事件的。在Reactor模型中,主要有四个角色:客户端连接,Reactor,Acceptor和Handler。这里Acceptor会不断地接收客户端的连接,然后将接收到的连接交由Reactor进行分发,最后有具体的Handler进行处理。**改进后的Reactor模型相对于传统的IO模型主要有如下优点:

  • 从模型上来讲,Reactor模型是以事件进行驱动的,其能够将接收客户端连接+ 网络读和网络写,以及业务计算进行拆分,从而极大的提升处理效率;
  • Reactor模型是异步非阻塞模型,工作线程在没有网络事件时可以处理其他的任务,而不用像传统IO那样必须阻塞等待。

Reactor模型----业务处理与IO分离以及并发读写

​ 由于网络读写和业务操作都在同一个线程中,在高并发情况下,会存在一定的系统瓶颈,可以通过使用线程池的方式处理业务操作。

瓶颈主要在两方面:

  • 高频率的网络读写事件处理;
  • 大量的业务操作处理;

在多线程进行业务操作的模型下,该模式主要具有如下特点:

  • 使用一个线程进行客户端连接的接收以及网络读写事件的处理;
  • 在接收到客户端连接之后,将该连接交由线程池进行数据的编解码以及业务计算。

其他扩展

Netty零拷贝

​ 零拷贝是指避免在用户态(User-space) 与内核态(Kernel-space) 之间来回拷贝数据的技术。

​ **JDK1.4加入了NIO机制和直接内存,目的是防止Java堆和Native堆之间数据复制带来的性能损耗,此后NIO可以使用Native的方式直接在 Native堆分配内存,也就是说直接使用堆外内存。**但是堆外内存溢出很难排查,需要手工清理。也可以通过安装插件来监控,避免内存溢出。

NIO提供MappedByteBuffer文件内存映射方案,其使用的也是直接内存。但是不受GC管控,后续Java提供了DirectByteBuffer类,它实现了DirectBuffer接口,维护一个Cleaner对象来完成内存回收。

1)FileChannel

​ 文件传输FileChannel是使用transferTo方法将数据从FileChannel对象传送到可写的字节通道(如Socket Channel等)。在transferTo方法内部实现中,由native方法transferTo0来实现,它依赖底层操作系统的支持。

​ 在UNIX和Linux系统中,调用transferTo这个方法会引起sendfile()系统调用,实现了数据直接从内核的读缓冲区传输到套接字缓冲区,避免了用户态(User-space) 与内核态(Kernel-space) 之间的数据拷贝。

​ **FileChannel提供了map方法来把文件映射为内存对象。**另外Java 中设计MappedByteBuffer就是为大文件准备的。

如果其后续要进行修改,还是通过SocketChannel发送,还是需要CPU进行数据的拷贝。

RocketMQ和Kafka对比

RocketMQ 选择了 mmap + write 这种零拷贝方式,适用于业务级消息这种小块文件的数据持久化和传输;

Kafka 采用的是 sendfile 这种零拷贝方式,适用于系统日志消息这种高吞吐量的大块文件的数据持久化和传输。Kafka 的索引文件使用的是 mmap + write 方式,数据文件使用的是 sendfile 方式。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值