Selector是Java NIO的一个组件,可以检查一个或者多个NIO channel,并且决定哪个channel已经准备好去读写了。通过这种方式,单个线程可以管理多个channel,当然也可以管理多个网络连接。
为什么使用selector ?
使用单个线程处理多个channel的优势是,你只需要很多的线程去处理大量channel。实际上,你可以只使用一个线程来处理所有的channel。对操作系统来说切换线程是需要大量开销的,而且每个线程都会占用一些资源(内存)。所以你使用的线程越少越好(译者注:达到目的的情况下,尽可能使用少的线程)。
不过要记住,当代操作系统和CPU的多任务处理性能已经越来越高了,所以多线程的开销已经变得越来越小了。实际上,如果CPU有多核,你不进行多任务处理则是对CPU性能的浪费。不管怎么样,关于这部分内容的讨论会在其他教程中给出。但是足够的是,你可以使用selector来达到使用单线程处理多个channel的目的。
下面的示意图是表示单个线程来处理三个channel:
创建Selector
通过调用Selector.open()
来创建Selector:
Selector selector = Selector.open();
使用Selector注册Channel
为了通过Selector
来使用Channel
,你需要使用Selector
来注册Channel
。这可以通过调用SelectableChannel.register()
方法来实现,如下所示:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
Channel要被Selector使用,必须要是非阻塞(non-blocking)模式。这意味着你不能使用Selector来管理FileChannel
,因为它不能选择非阻塞模式。但是socket channel是没有问题的。
注意register()
的第二个参数。这是一个“偏好设置”,意思是你向监听channel的什么事件,通过Selector.
你可以选择不同的监听事件,共有四种不同的监听事件:
- Connect
- Accept
- Read
- Write
所以,一个channel已经成功连接到另一个server就是上面的connect事件。一个server socket正在等待一个连接接入的过程就是上面的accept事件。一个channel有数据准备好去被读取就是read事件。一个channel已经准备好让你往里面写入数据,这就是write事件。
这四个事件用下面四个常量来表示:
SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE
如果你想注册多个事件,可以像下面这样:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectionKey's
如上面所述,当你用selector
注册一个channel,register()
方法会返回一个SelectionKey
对象。这个对象包含一些有意思的属性:
- interest set
- ready set
- Channel
- Selector
- 附加对象 (optional)
下面来具体介绍这些属性。
Interest Set
Interest Set表示的是你扫描的事件的集合。你可以通过SelectionKey来读写这个interest set像下面这样:
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;
如你所见,你可以&
操作给出的SelectionKey常量,去找出某一事件是否在interest set中。
Ready Set
ready set是channel准备好的操作的集合。主要是在selection后访问ready set。至于selection,会在后面给出解释。你可以像下面这样访问ready set:
int readySet = selectionKey.readyOps();
你可以用interest set以同样的方式来测试channe已经做好什么事件的准备了。但是,你可以利用下面的四个方法来代替,它们都会返回一个布尔值:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
Channel 和 Selector
从SelectionKey来访问channel和seector是无关紧要的,下面展示了如何进行此操作:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
附加对象
你可以将一个对象附加到SelectionKey中,或者给出更多的信息添加至channel,这可以很方便的途径去识别一个给定的channel。例如,你可以添加使用的buffer,或者一个包含更多数据的对象。下面是相关例子:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
你也可以使用register()
方法,在使用Selector注册channel时添加这个对象。如下所示:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
通过Selector选择Channel
当你用Selector
注册了一个或多个channel,你可以调用select()
方法以及其重载的方法。这些方法会返回你监听的事件(connect, accept, read 和 write)。换句话说,你可以从这个方法接受到已经准备好的事件。下面是select()方法以及其相关重载:
int select()
int select(long timeout)
int selectNow()
select()方法会一直阻塞,直到至少一个你注册的channel已经准备好相应事件。
select(long timeout)和select()方法做了同样的事儿,不过它是阻塞一段时间(多长时间取决于你传入的事件参数)。
selectNow() 一点也不会阻塞。不管channel准备好没有,它立即回返回。
select() 方法返回的int值表示有多少channel已经准备好。即:在上一次调用select()方法之后一共有多少channel已经准备好。如果你调用select()方法,它返回 1,因为此时只有一个channel准备好,那么你再一次调用select(),然后期间又有一个channel准备好,它会再返回1,而不是2。如果你没有对第一个准备好的channel有任何操作,你现在其实已经有两个准备好的channel了,但是只有一个channel是你两次调用之间准备好的channel。
selectedKeys()
调用select()方法之后,它会返回一个int值,这表示一个或多个channel已经准备好了,你可以通过“selected key set”访问准备好的channel,即调用selectedKeys()
方法:下面是如何操作的:
Set selectedKeys = selector.selectedKeys();
当你注册一个channel时候,Channel.register()返回一个SelectionKey
对象。这个key表示表示的是这个channel使用了哪个selector来注册的。可以通过selectedKeySet()
方法来访问key。
你可以迭代它来访问准备好的channel:
Set selectedKeys = selector.selectedKeys();
Iterator 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集合中的key。她再测试每个key以便去确定通过key引用的channel是否已经准备好。
注意,在循环的最后调用了keyIterator.remove()
方法。Selector本身并没有从selected key集合中删除SelectionKey实例。当你操作完channel后你需要这么做。下次channel准备好后,selector会再次将其添加至select key集合中。
方法SelectionKey.channel()
返回的channel可以被强转成你需要使用的channel,例如ServerSocketChannel
o或 SocketChannel
。
wakeUp()
调用select()
方法的线程被阻塞后,你可以让这个线程离开select()方法,即使channel没有准备好呢。这需要另一个线程在selector上调用Selector.wakeup()
方法。
如果一个不同的线程调用了wakeup()方法,并且当前已经没有线程在select()阻塞了,那么下一个调用select()方法的线程会立即唤醒(也就是不阻塞了)。
close()
当使用完Selector你可以调用她的close()
方法。这个操作会关闭selector,并且使所有在此selector上注册的SelectKey实例失效。channel本身并没有关闭。
完整的Selector例子
下面是一个完成的例子,包括打开Selector,并使用其注册channel(channel实例化不考虑),然后保持selector监控四个事件(accept, connect, read, write)。
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
int readyChannels = selector.select();
if(readyChannels == 0) continue;
Set selectedKeys = selector.selectedKeys();
Iterator 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();
}
}
想要查看此教程的目录请点击:Java NIO教程目录贴地址