【JAVA】【NIO】七、Java NIO Selector

【JAVA】【NIO】7、Java NIO Selector

selector是Java NIO的组件可以检查一个或多个NIO的channel,并且决定哪个channel是为读写准备好了。这种方式,单个线程可以管理多个channel,也就是多个网络连接。

为什么使用选择器

优点就是更少的线程去处理多个通道。实际上,你可以使用一个线程去处理所有的通道。操作系统中线程的切换是很费资源的,而且每个线程本身也占用了一些资源(内存)。所以使用的线程越少越好!

现在的操作系统和CPU在多任务上变得越来越好,所以多线程的开销也变得更小了。事实上,如果一个CPU有多个核心,不用多线程可能是一种浪费。不管怎么说,设计讨论应该是在另一篇文章说。在这里,知道用selector,单线程去处理多通道就足够了。
【JAVA】【NIO】七、Java NIO Selector

创建选择器

通过调用Selector.open()方法创建。

注册通道

channel.configureBlocking(false);配置通道为非阻塞模式
channel.register(selector,SelectionKey.OP_ACCEPT);通过该方法注册

使用selector,channel必须是非阻塞模式。意味着FileChannel不用使用selector,因为FileChannel不能转为非阻塞模式。SocketChannel可以正常使用。

注意register方法的第二个参数。这是一个兴趣集合,意思是通过selector监听这个channel时,对什么样的事件感兴趣。有如下几种:
1、Connect
2、Accept
3、Read
4、Write

一个通道触发了一个事件意思就是对该事件准备就绪了。所以,一个channel和服务器连接成功了就是连接就绪。ServerSocketChannel接受了连接就是接受就绪。一个通道有数据准备好被读了就是读就绪。一个通道准备写入数据就是写就绪。
这四中事件通过SelectionKey的四个常量来定义:
1、SelectionKey.OP_CONNECT
2、SelectionKey.OP_ACCEPT
3、SelectionKey.OP_READ
4、SelectionKey.OP_WRITE
如果你对多个事件有兴趣,可以如下来写:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

在文章靠后将会回到兴趣集来讲解。

SelectionKey’s

正如前面所述,当你通过selector给通道注册,register方法将返回一个SelectionKey对象,这个对象包含了一些你感兴趣的属性:
·The interest set
·The ready set
·The Channel
·The Selector
·An attached object (optional)

Interest Set

interest集合是你感兴趣的事件集合。你可以通过SelectionKey读写兴趣集合,如下:

int interestSet = selectionKey.interestOps();

boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;

你可以通过&操作找出某个事件是否是兴趣集合的。

Ready Set

ready集合是channel为哪些操作已经就绪了。在一次选择后,你会首先访问ready集合,如下:

int readySet = selectionKey.readyOps();

同样你可以像上面提到的方法一样通过&来测试哪些操作已经就绪了。但是同样你可以通过如下方法来得到:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

Channel + Selector

通过SelectionKey访问channel+selector很简单,如下:

Channel channel = selectionKey.channel();

Selector selector = selectionKey.selector();

Attaching Objects

可以将一个对象或者更多信息附加到SelectionKey上以便识别一个具体的通道。例如,你可以附加和通道一起使用的Buffer或者一个包含很多聚集数据的对象,如下:

selectionKey.attach(theObject);

Object attachedObj = selectionKey.attachment();

你也可以再register的时候就附加对象

SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

Selecting Channels via a Selector

一旦你通过selector注册了多个通道,你可以调用selecto方法。这些方法返回为兴趣集合(连接,接收,读,写)事件就绪的通道。换言之,如果你对对读就绪的通道感兴趣, 你就会通过select方法返回读就绪的通道。
以下是select方法:
·int select()
·int select(long timeout)
·int selectNow()
select()方法会阻塞直到至少一个通道是为你注册的事件就绪的。
select(long timeout)和select()一样,除了它阻塞会有一个超时时间。
selectNow()没有阻塞,无论什么通道,就绪就立刻返回。
select()方法返回的int表示有多少通道就绪了,即自从最后一次调用select()方法以来,有多少通道就绪了。如果你调用select方法返回1,说明有一个通道就绪了,你再次调用返回1,说明另一个通道就绪了。如果你对第一个就绪的通道什么都不做,你现在就有两个就绪通道,但是仅仅只有一个通道就绪在每次select方法调用过程中。

selectedKeys()

一旦你调用了一个select()方法并且返回值,表明一个或多个通道就绪了,你可以通过selected key集合访问就绪通道,通过调用selectedKeys方法实现,如下:

Set selectedKeys = selector.selectedKeys();

当你注册了一个通道事件时会返回一个SelectionKey对象。这个对象表示注册到该Selector上的通道。你可以通过selectedKeySet访问这些keys。如下:

Set<SelectionKey> selectedKeys = selector.selectedKeys();

Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

while(keyIterator.hasNext()) {

    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.

    } else if (key.isConnectable()) {
        // a connection was established with a remote server.

    } else if (key.isReadable()) {
        // a channel is ready for reading

    } else if (key.isWritable()) {
        // a channel is ready for writing
    }

    keyIterator.remove();
}

这个循环遍历selected key集合。并检测各个键对应的通道就绪事件。
注意每次迭代末尾的keyIterator.remove()调用。Selector不会自己从已选择键集中移除SelectionKey实例。必须在处理完通道时自己移除。下次该通道变成就绪时,Selector会再次将其放入已选择键集中。

SelectionKey.channel()方法返回的channel需要转型成你需要的,例如ServerSocketChannel,SocketChannel等。

wakeUp()

一个线程调用select方法阻塞了,即使没有就绪通道,也可以让select方法返回。让其它线程通过刚刚调用select方法的Selector对象调用wakeup方法即可。阻塞在select方法上的线程会立即返回。
如果其它线程调用了wakeup,但是当前没有线程阻塞在select,那么下一个调用select方法的线程会立即唤醒。

close()

用完Selector要调用close方法。关闭Selector并且将所有注册在selector上的键集作废。通道自己不会关闭。

完整实例

package nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.util.Iterator;
import java.util.Set;

public class SelectorDemo {

    public static void main(String[] args) throws IOException {
        //打开服务器套接字通道
        ServerSocketChannel ssc = ServerSocketChannel.open();
        //非阻塞模式
        ssc.configureBlocking(false);
        //获取与此通道关联的服务器套接字
        ServerSocket ss = ssc.socket();
        //服务绑定
        ss.bind(new InetSocketAddress(8990));
        //打开一个选择器
        Selector selector = Selector.open();
        //在该选择器上注册通道事件
        SelectionKey registerKey = ssc.register(selector, SelectionKey.OP_ACCEPT);
        while(true) {
            int readyChannels = selector.select();
            if(readyChannels==0) {
                System.out.println("No Channel Is Ready !");
                continue;
            }
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
            while(keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                if(key.isAcceptable()) {
                    System.out.println("接收操作!");
                }else if(key.isConnectable()) {
                    System.out.println("连接操作!");
                }else if(key.isReadable()) {
                    System.out.println("读操作!");
                }else if(key.isWritable()) {
                    System.out.println("写操作!");
                }
                keyIterator.remove();
            }
        }
    }

}




以上代码注意,在注册事件时只能是ACCEPT,其它事件在外面注册都会导致程序运行失败,因为其它所有事件都是在ACCEPT后才能够注册的,所以要注意这一点。

下一节:等待