NIO是什么

为了实现Selector管理多个SocketChannel,必须将具体的SocketChannel对象注册到Selector,并声明需要监听的事件(这样Selector才知道需要记录什么数据),一共有4种事件:
1、connect:客户端连接服务端事件,对应值为SelectionKey.OPCONNECT(8) 
2、accept:服务端接收客户端连接事件,对应值为SelectionKey.OPACCEPT(16) 
3、read:读事件,对应值为SelectionKey.OPREAD(1) 
4、write:写事件,对应值为SelectionKey.OPWRITE(4)

Selector用一个线程轮询多个Channel,如果发现某个channel发生了在selector上注册的事件,则用该线程进行处理该事件,
如果是read读事件,则该线程读取该channel中buffer里的数据

Selector 将检测到有通信信道I/O有数据传输时,通过selelct()取得SocketChannel,将数据读取或写入该Channel的Buffer缓冲区中

------------------------------

1. 阻塞I/O通信模型 

假如现在你对阻塞I/O已有了一定了解,我们知道阻塞I/O在调用InputStream.read()方法时是阻塞的,它会一直等到数据到来时(或超时)才会返回;同样,在调用ServerSocket.accept()方法时,也会一直阻塞到有客户端连接才会返回,每个客户端连接过来后,服务端都会启动一个线程去处理该客户端的请求。阻塞I/O的通信模型示意图如下:

 

 

如果你细细分析,一定会发现阻塞I/O存在一些缺点。根据阻塞I/O通信模型,我总结了它的两点缺点:
1. 当客户端多时,会创建大量的处理线程。且每个线程都要占用栈空间和一些CPU时间
2. 阻塞可能带来频繁的上下文切换,且大部分上下文切换可能是无意义的。
在这种情况下非阻塞式I/O就有了它的应用前景。

2. java NIO原理及通信模型 
Java NIO是在jdk1.4开始使用的,它既可以说成“新I/O”,也可以说成非阻塞式I/O。下面是java NIO的工作原理:
1. 由一个专门的线程来处理所有的 IO 事件,并负责分发。 
2. 事件驱动机制:事件到的时候触发,而不是同步的去监视事件。 

https://weixiaolu.iteye.com/blog/1479656

-------------------------

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

理解了这些概念后我们看一下,实际上它们是如何工作的,下面是典型的一段 NIO 代码:

清单 2. NIO 工作代码示例

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

public void selector() throws IOException {

        ByteBuffer buffer = ByteBuffer.allocate(1024);

        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 selectedKeys = selector.selectedKeys();//取得所有key集合

            Iterator it = selectedKeys.iterator();

            while (it.hasNext()) {

                SelectionKey key = (SelectionKey) 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);

                    it.remove();

                } else if

                ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {

                    SocketChannel sc = (SocketChannel) key.channel();

                    while (true) {

                        buffer.clear();

                        int n = sc.read(buffer);//读取数据

                        if (n <= 0) {

                            break;

                        }

                        buffer.flip();

                    }

                    it.remove();

                }

            }

        }

}

调用 Selector 的静态工厂创建一个选择器,创建一个服务端的 Channel 绑定到一个 Socket 对象,并把这个通信信道注册到选择器上,把这个通信信道设置为非阻塞模式。然后就可以调用 Selector 的 selectedKeys 方法来检查已经注册在这个选择器上的所有通信信道是否有需要的事件发生,如果有某个事件发生时,将会返回所有的 SelectionKey,通过这个对象 Channel 方法就可以取得这个通信信道对象从而可以读取通信的数据,而这里读取的数据是 Buffer,这个 Buffer 是我们可以控制的缓冲器。

在上面的这段程序中,是将 Server 端的监听连接请求的事件和处理请求的事件放在一个线程中,但是在实际应用中,我们通常会把它们放在两个线程中,一个线程专门负责监听客户端的连接请求,而且是阻塞方式执行的;另外一个线程专门来处理请求,这个专门处理请求的线程才会真正采用 NIO 的方式,像 Web 服务器 Tomcat 和 Jetty 都是这个处理方式,关于 Tomcat 和 Jetty 的 NIO 处理方式可以参考文章《 Jetty 的工作原理和与 Tomcat 的比较》。

下图是描述了基于 NIO 工作方式的 Socket 请求的处理过程:

图 10. 基于 NIO 的 Socket 请求的处理过程

图 10. 基于 NIO 的 Socket 请求的处理过程

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

Buffer 的工作方式

上面介绍了 Selector 将检测到有通信信道 I/O 有数据传输时,通过 selelct() 取得 SocketChannel,将数据读取或写入 Buffer 缓冲区。

通过 Channel 获取的 I/O 数据首先要经过操作系统的 Socket 缓冲区再将数据复制到 Buffer 中,这个的操作系统缓冲区就是底层的 TCP 协议关联的 RecvQ 或者 SendQ 队列,从操作系统缓冲区到用户缓冲区复制数据比较耗性能,Buffer 提供了另外一种直接操作操作系统缓冲区的的方式即 ByteBuffer.allocateDirector(size),这个方法返回的 byteBuffer 就是与底层存储空间关联的缓冲区,它的操作方式与 linux2.4 内核的 sendfile 操作方式类似。

以下是我理解的数据流动过程:

https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html

客户端与服务器建立socket通信连接后,客户端和服务器端均会有socket缓冲区,并且每个socket缓冲区均包含接收队列和发送队列来传输数据。

发布了815 篇原创文章 · 获赞 318 · 访问量 193万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览