NIO工作方式

来源:互联网 发布:淘客网页源码 编辑:程序博客网 时间:2024/06/17 23:44

NIO工作机制

NIO相关类图

如上图,NIO有两个关键类:ChannelSelector,我们可采用城市交通工具来比喻NIO的工作方式,这里的Channel比Socket更加具体,它可以比作某种具体的交通工具,如汽车或高铁,而Selector可比作车站的车辆运行调度系统,它负责监控每辆车的当前运行状态,是已经出站还是在路上等。也就是它可以轮询每个Channel的状态. 这里还有一个 Buffer 类,它也比 Stream 更加具体化,我们可以将它比作为车上的座位,Channel 是汽车的话就是汽车上的座位,高铁上就是高铁上的座位,它始终是一个具体的概念,与 Stream 不同。Stream 只能代表是一个座位,至于是什么座位由你自己去想象,也就是你在去上车之前并不知道,这个车上是否还有没有座位了,也不知道上的是什么车,因为你并不能选择,这些信息都已经被封装在了运输工具(Socket)里面了,对你是透明的。NIO 引入了 Channel、Buffer 和 Selector 就是想把这些信息具体化,让程序员有机会控制它们,如:当我们调用 write() 往 SendQ 写数据时,当一次写的数据超过 SendQ 长度是需要按照 SendQ 的长度进行分割,这个过程中需要有将用户空间数据和内核地址空间进行切换,而这个切换不是你可以控制的。而在 Buffer 中我们可以控制 Buffer 的 capacity,并且是否扩容以及如何扩容都可以控制。
理解了这些概念后我们看一下,实际上它们是如何工作的,下面是典型的一段 NIO 代码:

public void testSelector() {    ByteBuffer byteBuffer = ByteBuffer.allocate(BUFFER_SIZE);    try {        Selector selector = Selector.open();        ServerSocketChannel ssc = ServerSocketChannel.open();        ssc.configureBlocking(false);        ssc.socket().bind(new InetSocketAddress(8080));        ssc.register(selector, SelectionKey.OP_ACCEPT);        while (true) {            Set<SelectionKey> selectedKeys = selector.selectedKeys();            Iterator<SelectionKey> it = selectedKeys.iterator();            while (it.hasNext()) {                SelectionKey key = it.next();                if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {                    ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();                    SocketChannel sc = ssChannel.accept();                    sc.configureBlocking(false);                    sc.register(selector, SelectionKey.OP_READ);                } else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {                    SocketChannel sc = (SocketChannel) key.channel();                    while (true) {                        byteBuffer.clear();                        int n = sc.read(byteBuffer);                        if (n <= 0)                            break;                        byteBuffer.flip();                    }                }                it.remove();            }        }    } catch (Exception e) {        e.printStackTrace();    }}

调用 Selector 的静态工厂创建一个选择器,创建一个服务端的 Channel 绑定到一个 Socket 对象,并把这个通信信道注册到选择器上,把这个通信信道设置为非阻塞模式。然后就可以调用 Selector 的 selectedKeys 方法来检查已经注册在这个选择器上的所有通信信道是否有需要的事件发生,如果有某个事件发生时,将会返回所有的 SelectionKey,通过这个对象 Channel 方法就可以取得这个通信信道对象从而可以读取通信的数据,而这里读取的数据是 Buffer,这个 Buffer 是我们可以控制的缓冲器。
在上面的这段程序中,是将 Server 端的监听连接请求的事件和处理请求的事件放在一个线程中,但是在实际应用中,我们通常会把它们放在两个线程中,一个线程专门负责监听客户端的连接请求,而且是阻塞方式执行的;另外一个线程专门来处理请求,这个专门处理请求的线程才会真正采用 NIO 的方式,像 Web 服务器 Tomcat 和 Jetty 都是这个处理方式,关于 Tomcat 和 Jetty 的 NIO 处理方式可以参考文章《 Jetty 的工作原理和与 Tomcat 的比较》。
基于 NIO 工作方式的 Socket 请求的处理过程:

  • Selector 可以同时监听一组通信信道(Channel)上的 I/O 状态,前提是这个 Selector 要已经注册到这些通信信道中 ;
  • 选择器 Selector 可以调用 select() 方法检查已经注册的通信信道上的是否有 I/O 已经准备好,如果没有至少一个信道 I/O 状态有变化,那么 select 方法会阻塞等待或在超时时间后会返回 0;
  • 如果有多个信道有数据,那么将会将这些数据分配到对应的数据 Buffer 中。所以关键的地方是有一个线程来处理所有连接的数据交互,每个连接的数据交互都不是阻塞方式,所以可以同时处理大量的连接请求。

Buffer的工作方式

前面介绍了Selector检测到通信信道I/O有数据传输时,通过select()取得SocketChannel,将数据读入或写入Buffer缓冲区,那么Buffer是是如何接收和写出数据的?
Buffer 可以简单的理解为一组基本数据类型的元素列表,它通过几个变量来保存这个数据的当前位置状态,也就是有四个索引。

  • capacity 缓冲区数组的总长度
  • position 下一个要操作的数据元素的位置
  • limit 缓冲区数组中不可操作的下一个元素的位置,limit<=capacity
  • mark 用于记录当前 position 的前一个位置或者默认是 0

实际操作数据时它们的关系如下图:

这里写图片描述

我们通过 ByteBuffer.allocate(11) 方法创建一个 11 个 byte 的数组缓冲区,初始状态如上图所示,position 的位置为 0,capacity 和 limit 默认都是数组长度。当我们写入 5 个字节时位置变化如下图所示:

这里写图片描述

这时我们需要将缓冲区的 5 个字节数据写入 Channel 通信信道,所以我们需要调用 byteBuffer.flip() 方法,数组的状态又发生如下变化:

这里写图片描述

这时底层操作系统就可以从缓冲区中正确读取这 5 个字节数据发送出去了。在下一次写数据之前我们在调一下 clear() 方法。缓冲区的索引状态又回到初始位置。

这里还要说明一下 mark,当我们调用 mark() 时,它将记录当前 position 的前一个位置,当我们调用 reset 时,position 将恢复 mark 记录下来的值。

还有一点需要说明,通过 Channel 获取的 I/O 数据首先要经过操作系统的 Socket 缓冲区再将数据复制到 Buffer 中,这个的操作系统缓冲区就是底层的 TCP 协议关联的 RecvQ 或者 SendQ 队列,从操作系统缓冲区到用户缓冲区复制数据比较耗性能,Buffer 提供了另外一种直接操作操作系统缓冲区的的方式即 ByteBuffer.allocateDirector(size),这个方法返回的 DirectByteBuffer就是与底层存储空间关联的缓冲区,它通过Native代码操作非JVM堆的内存空间,每次创建或释放都调用System.gc()。注意,使用DirectByteBuffer可能会引起内存泄漏,下表是DirectByteBuffer和HEapByteBuffer的对比

这里写图片描述