Java NIO通俗编程之选择器Selector(四)

来源:互联网 发布:大数据信息化调研提纲 编辑:程序博客网 时间:2024/04/27 23:36

一、介绍

选择器提供选择执行已经就绪的任务的能力.从底层来看,Selector提供了询问通道是否已经准备好执行每个I/O操作的能力。Selector 允许单线程处理多个Channel。仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道,这样会大量的减少线程之间上下文切换的开销。
如果正在处理事件时,有新的连接要接入,那么新的连接还是要等待吗?要是某个事件处理的时间非常长,那新连接是不是要一直等待?要是某个事件处理的时间非常长,新连接是要一直等待的,所以NIO的多路复用适合大量短连接的处理场景。
       通常在进行同步I/O操作时,如果读取数据,代码会阻塞直至有 可供读取的数据。同样,写入调用将会阻塞直至数据能够写入。传统的Server/Client模式会基于TPR(Thread per Request),服务器会为每个客户端请求建立一个线程,由该线程单独负责处理一个客户请求。这种模式带来的一个问题就是线程数量的剧增,大量的线程会增大服务器的开销。大多数的实现为了避免这个问题,都采用了线程池模型,并设置线程池线程的最大数量,这由带来了新的问题,如果线程池中有200个线程,而有200个用户都在进行大文件下载,会导致第201个用户的请求无法及时处理,即便第201个用户只想请求一个几KB大小的页面。NIO是把所有的请求有关IO事件全部打包到列表里面,然后用一个线程去处理串行所有的IO操作,从请求的角度看请求就会发现,请求变得好慢,特别是最后一个请求等的挺蛋疼的。传统的 Server/Client模式如下图所示:



在开始之前,需要回顾一下Selector、SelectableChannel和SelectionKey:

1. 选择器(Selector)
Selector选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器是一起被注册的,并且使用选择器来更新通道的就绪状态。当这么做的时候,可以选择将被激发的线程挂起,直到有就绪的的通道。

2. 可选择通道(SelectableChannel)
SelectableChannel这个抽象类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。因为FileChannel类没有继承SelectableChannel因此是不是可选通道,而所有socket通道都是可选择的,包括从管道(Pipe)对象的中获得的通道。SelectableChannel可以被注册到Selector对象上,同时可以指定对那个选择器而言,哪种操作是感兴趣的。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。

3. 选择键(SelectionKey)
选择键封装了特定的通道与特定的选择器的注册关系。选择键对象被SelectableChannel.register()返回并提供一个表示这种注册关系的标记。选择键包含了两个比特集(以整数的形式进行编码),指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。

下面是使用Selector管理多个channel的结构图: 

从图中可以看出,当有读或写等任何注册的事件发生时,可以从Selector中获得相应的SelectionKey,同时从 SelectionKey中可以找到发生的事件和该事件所发生的具体的SelectableChannel,以获得客户端发送过来的数据。
注册的伪代码如下:
SelectionKey key1 = channel1.register(selector,SelectionKey.OP_WRITE);SelectionKey key2 = channel2.register(selector,SelectionKey.OP_READ);SelectionKey key3 = channel3.register(selector,SelectionKey.OP_READ | SelectionKey.OP_READ);
解释如下:
channel1注册到selector上面去,并且selector告诉channel1说,我只对你的写事件感兴趣。
channel2注册到selector上面去,并且selector告诉channel2说,我只对你的读事件感兴趣。
channel3注册到selector上面去,并且selector告诉channel3说,我只对你的读和写事件感兴趣。

二、Selector的使用

1. 创建Selector
Selector对象是通过调用静态工厂方法open()来实例化的,如下:
Selector Selector = Selector.open();
类方法open()实际上向SPI1发出请求,通过默认的SelectorProvider对象获取一个新的实例。

2. 将Channel注册到Selector
要实现Selector管理Channel,需要将channel注册到相应的Selector上,如下:
channel.configureBlocking(false);SelectionKey key= channel.register(selector,SelectionKey,OP_READ);
通过调用通道的register()方法会将它注册到一个选择器上。与Selector一起使用时,Channel必须处于非阻塞模式下,否则将抛出IllegalBlockingModeException异常,这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式,而套接字通道都可以。另外通道一旦被注册,将不能再回到阻塞状态,此时若调用通道的configureBlocking(true)将抛出BlockingModeException异常。

register()方法的第二个参数是“interest集合”,表示选择器所关心的通道操作,它实际上是一个表示选择器在检查通道就绪状态时需要关心的操作的比特掩码。比如一个选择器对通道的read和write操作感兴趣,那么选择器在检查该通道时,只会检查通道的read和write操作是否已经处在就绪状态。 
它有以下四种操作类型:
Connect 连接
Accept 接受
Read 读
Write 写
需要注意并非所有的操作在所有的可选择通道上都能被支持,比如ServerSocketChannel支持Accept,而SocketChannel中不支持。我们可以通过通道上的validOps()方法来获取特定通道下所有支持的操作集合。
JAVA中定义了四个常量来表示这四种操作类型:
SelectionKey.OP_CONNECTSelectionKey.OP_ACCEPTSelectionKey.OP_READSelectionKey.OP_WRITE

如果Selector对通道的多操作类型感兴趣,可以用“位或”操作符来实现:
int interestSet=SelectionKey.OP_READ|SelectionKey.OP_WRITE; 
当通道触发了某个操作之后,表示该通道的某个操作已经就绪,可以被操作。因此,某个SocketChannel成功连接到另一个服务器称为“连接就绪”(OP_CONNECT)。一个ServerSocketChannel准备好接收新进入的连接称为“接收就绪”(OP_ACCEPT)。一个有数据可读的通道可以说是“读就绪”(OP_READ)。等待写数据的通道可以说是“写就绪”(OP_WRITE)。
我们注意到register()方法会返回一个SelectionKey对象,我们称之为键对象。该对象包含了以下四种属性:
interest集合
read集合
Channel
Selector
interest集合是Selector感兴趣的集合,用于指示选择器对通道关心的操作,可通过SelectionKey对象的interestOps()获取。最初,该兴趣集合是通道被注册到Selector时传进来的值。该集合不会被选择器改变,但是可通过interestOps()改变。我们可以通过以下方法来判断Selector是否对Channel的某种事件感兴趣:
int interestSet=selectionKey.interestOps();boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
read集合是通道已经就绪的操作的集合,表示一个通道准备好要执行的操作了,可通过SelctionKey对象的readyOps()来获取相关通道已经就绪的操作。它是interest集合的子集,并且表示了interest集合中从上次调用select()以后已经就绪的那些操作。(比如选择器对通道的read,write操作感兴趣,而某时刻通道的read操作已经准备就绪可以被选择器获知了,前一种就是interest集合,后一种则是read集合。)。
JAVA中定义以下几个方法用来检查这些操作是否就绪:
    selectionKey.isAcceptable();//等价于selectionKey.readyOps()&SelectionKey.OP_ACCEPT    selectionKey.isConnectable();    selectionKey.isReadable();    selectionKey.isWritable();
需要注意的是,通过相关的选择键的readyOps()方法返回的就绪状态指示只是一个提示,底层的通道在任何时候都会不断改变,而其他线程也可能在通道上执行操作并影响到它的就绪状态。另外,我们不能直接修改read集合。

取出SelectionKey所关联的Selector和Channel :
Channel channel = selectionKey.channel();Selector selector = selectionKey.selector();

我们可以通过SelectionKey对象的cancel()方法来取消特定的注册关系。该方法调用之后,该SelectionKey对象将会被”拷贝”至已取消键的集合中,该键此时已经失效,但是该注册关系并不会立刻终结。在下一次select()时,已取消键的集合中的元素会被清除,相应的注册关系也真正终结。

三、编写服务端程序的基本步骤

使用NIO中非阻塞I/O编写服务器处理程序,大体上可以分为下面三个步骤:
1. 在将channel注册到selector上面时,同时Selector对象需要告诉channel对象,selector感兴趣的事件。
2. 从Selector中获取感兴趣的事件。 
3. 根据不同的事件进行相应的处理。
接下来我们用一个简单的示例来说明整个过程。
/* * 注册事件 * */protected Selector getSelector() throws IOException {    // 创建Selector对象    Selector sel = Selector.open();        // 创建可选择通道,并配置为非阻塞模式    ServerSocketChannel server = ServerSocketChannel.open();    server.configureBlocking(false);        // 绑定通道到指定端口    ServerSocket socket = server.socket();    InetSocketAddress address = new InetSocketAddress(port);    socket.bind(address);        // 向Selector中注册感兴趣的事件    server.register(sel, SelectionKey.OP_ACCEPT);     return sel;}
创建了ServerSocketChannel对象,并调用configureBlocking()方法,配置为非阻塞模式,接下来的三行代码把该通道绑定到指定端口,最后向Selector中注册事件,此处指定的是参数是OP_ACCEPT,即指定我们想要监听accept事件,也就是新的连接发 生时所产生的事件,对于ServerSocketChannel通道来说,我们唯一可以指定的参数就是OP_ACCEPT。从Selector中获取感兴趣的事件,即开始监听,进入内部循环:
/* * 开始监听 * */ public void listen() {     System.out.println("listen on " + port);    try {         while(true) {             // 该调用会阻塞,直到至少有一个事件发生            selector.select();             Set<SelectionKey> keys = selector.selectedKeys();            Iterator<SelectionKey> iter = keys.iterator();            while (iter.hasNext()) {                 SelectionKey key = (SelectionKey) iter.next();                 iter.remove();                 process(key);             }         }     } catch (IOException e) {         e.printStackTrace();    } }
在非阻塞I/O中,内部循环模式基本都是遵循这种方式。首先调用select()方法,该方法会阻塞,直到至少有一个事件发生,然后再使用selectedKeys()方法获取发生事件的SelectionKey,再使用迭代器进行循环。
NIO是非阻塞的。但在listen里调用的selector.select(); 方法会阻塞。这和NIO非阻塞岂不是矛盾了?
非阻塞指的是 IO 事件本身不阻塞,但是获取 IO 事件的 select 方法是需要阻塞等待的.区别是阻塞的 IO 会阻塞在 IO 操作上, NIO 阻塞在事件获取上,没有事件就没有 IO, 从高层次看 IO 就不阻塞了.也就是说只有 IO 已经发生那么我们才评估 IO 是否阻塞,但是 select 阻塞的时候 IO 还没有发生,何谈 IO 的阻塞呢. NIO 的本质是延迟 IO 操作到真正发生 IO 的时候,而不是以前的只要 IO 流打开了就一直等待 IO 操作.

最后一步就是根据不同的事件,编写相应的处理代码:
/* * 根据不同的事件做处理 * */protected void process(SelectionKey key) throws IOException{    // 接收请求    if (key.isAcceptable()) {        ServerSocketChannel server = (ServerSocketChannel) key.channel();        SocketChannel channel = server.accept();        channel.configureBlocking(false);        channel.register(selector, SelectionKey.OP_READ);    }    // 读信息    else if (key.isReadable()) {        SocketChannel channel = (SocketChannel) key.channel();         int count = channel.read(buffer);         if (count > 0) {             buffer.flip();             CharBuffer charBuffer = decoder.decode(buffer);             name = charBuffer.toString();             SelectionKey sKey = channel.register(selector, SelectionKey.OP_WRITE);             sKey.attach(name);         } else {             channel.close();         }         buffer.clear();     }    // 写事件    else if (key.isWritable()) {        SocketChannel channel = (SocketChannel) key.channel();         String name = (String) key.attachment();                 ByteBuffer block = encoder.encode(CharBuffer.wrap("Hello " + name));         if(block != null)        {            channel.write(block);        }        else        {            channel.close();        }     }}
此处分别判断是接受请求、读数据还是写事件,分别作不同的处理。




参考文献:
1. Java NIO使用及原理分析 (四)
2. 通俗编程——白话NIO之Selector
3.【Java.NIO】Selector,及SelectionKey 
4. Java NIO类库Selector机制解析--转
5. Java NIO (六) Selector
6. NIO边看边记 之 selector选择器(六)
0 0
原创粉丝点击