selector是Java NIO的组件可以检查一个或多个NIO的channel,并且决定哪个channel是为读写准备好了。这种方式,单个线程可以管理多个channel,也就是多个网络连接。
为什么使用选择器
优点就是更少的线程去处理多个通道。实际上,你可以使用一个线程去处理所有的通道。操作系统中线程的切换是很费资源的,而且每个线程本身也占用了一些资源(内存)。所以使用的线程越少越好!
现在的操作系统和CPU在多任务上变得越来越好,所以多线程的开销也变得更小了。事实上,如果一个CPU有多个核心,不用多线程可能是一种浪费。不管怎么说,设计讨论应该是在另一篇文章说。在这里,知道用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后才能够注册的,所以要注意这一点。
下一节:等待