Java NIO Selector
- Why Use a Selector?
- Creating a Selector
- Registering Channels with the Selector
- SelectionKey's
- Interest Set
- Ready Set
- Channel + Selector
- Attaching Objects
- Selecting Channels via a Selector
- selectedKeys()
- wakeUp()
- close()
- Full Selector Example
A Selector
is a Java NIO component which can examine one or more NIO Channel's, and determine which channels are ready for e.g. reading or writing. This way a single thread can manage multiple channels, and thus multiple network connections.
Selector 是一个NIO的组件,它可以检查一个或多个channel,并决定哪个channel是否做好了读、写准备
Why Use a Selector?
The advantage of using just a single thread to handle multiple channels is that you need less threads to handle the channels. Actually, you can use just one thread to handle all of your channels. Switching between threads is expensive for an operating system, and each thread takes up some resources (memory) in the operating system too. Therefore, the less threads you use, the better.
可以用尽量少的线程,甚至一个线程处理多个channel。
线程之间切换耗费资源严重,而且每个线程本身就要分配系统资源,比如-xss指定的内存栈空间。
所以使用的线程越少越好,selector的出现就是为了减少线程的使用。
Keep in mind though, that modern operating systems and CPU's become better and better at multitasking, so the overheads of multithreading becomes smaller over time. In fact, if a CPU has multiple cores, you might be wasting CPU power by not multitasking. Anyways, that design discussion belongs in a different text. It suffices to say here, that you can handle multiple channels with a single thread, using a Selector
.
但是,需要记住,现代的操作系统和CPU在多任务方面表现的越来越好,所以多线程的开销随着时间的推移,变得越来越小了。实际上,如果一个CPU有多个内核,不使用多任务可能是在浪费CPU能力。不管怎么说,关于那种设计的讨论应该放在另一篇不同的文章中。在这里,只要知道使用Selector能够处理多个通道就足够了。
Here is an illustration of a thread using a Selector
to handle 3 Channel
's:
Java NIO: A Thread uses a Selector to handle 3 Channel's
Creating a Selector
You create a Selector
by calling the Selector.open()
method, like this:
Selector selector = Selector.open();
Registering Channels with the Selector
In order to use a Channel
with a Selector
you must register the Channel
with the Selector
. This is done using the SelectableChannel.register()
method, like this:
channel.configureBlocking(false); // 非阻塞模式
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
The Channel
must be in non-blocking mode to be used with a Selector
. This means that you cannot use FileChannel
's with a Selector
since FileChannel
's cannot be switched into non-blocking mode. Socket channels will work fine though
.与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而SocketChannel都可以。
Notice the second parameter of the register()
method. This is an "interest set", meaning what events you are interested in listening for in the Channel
, via the Selector
. There are four different events you can listen for:
- Connect
- Accept
- Read
- Write
A channel that "fires an event" is also said to be "ready" for that event.
So, a channel that has connected successfully to another server is "connect ready". — client端channel
A server socket channel which accepts an incoming connection is "accept" ready. —— server端 serverSocketChannel
A channel that has data ready to be read is "read" ready.
A channel that is ready for you to write data to it, is "write" ready.
These four events are represented by the four SelectionKey
constants:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
If you are interested in more than one event, OR the constants together, like this:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
I'll return to the interest set a bit further down in this text.
SelectionKey's
As you saw in the previous section, when you register a Channel
with a Selector
the register()
method returns a SelectionKey
objects. This SelectionKey
object contains a few interesting properties:
- The interest set
- The ready set
- The Channel
- The Selector
- An attached object (optional)
I'll describe these properties below.
Interest Set
The interest set is the set of events you are interested in "selecting", as described in the section "Registering Channels with the Selector". You can read and write that interest set via the SelectionKey
like this:
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;
As you can see, you can AND the interest set with the given SelectionKey
constant to find out if a certain event is in the interest set.
interest集合是你所选择的感兴趣的事件集合。可以通过SelectionKey读写interest集合
用“位与 & ”操作interest 集合和给定的SelectionKey常量,可以确定某个确定的事件是否在interest 集合中。
Ready Set
The ready set is the set of operations the channel is ready for. You will primarily be accessing the ready set after a selection.
ready 集合是通道已经准备就绪的操作的集合。在一次选择(Selection)之后,你会首先访问这个ready set
Selection is explained in a later section. You access the ready set like this:
int readySet = selectionKey.readyOps();
You can test in the same way as with the interest set, what events / operations the channel is ready for. But, you can also use these four methods instead, which all reaturn a boolean:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
Channel + Selector
Accessing the channel + selector from the SelectionKey
is trivial. Here is how it's done:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
从SelectionKey中获取channel和selector有直接的方法可以调用
Attaching Objects
You can attach an object to a SelectionKey
this is a handy way of recognizing a given channel, or attaching further information to the channel. For instance, you may attach the Buffer
you are using with the channel, or an object containing more aggregate data.
可以将一个对象或者更多信息附着到SelectionKey上,这样就能方便的识别某个给定的通道。例如,可以附加 与通道一起使用的Buffer,或是包含聚集数据的某个对象。
Here is how you attach objects:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
You can also attach an object already while registering the Channel
with the Selector
, in the register()
method. Here is how that looks:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
Selecting Channels via a Selector
Once you have register one or more channels with a Selector
you can call one of the select()
methods. These methods return the channels that are "ready" for the events you are interested in (connect, accept, read or write). In other words, if you are interested in channels that are ready for reading, you will receive the channels that are ready for reading from the select()
methods.
一旦向Selector注册了一或多个通道,就可以调用几个重载的select()方法。这些方法返回对所感兴趣的事件(如连接、接受、读或写)已经准备就绪的那些通道。换句话说,如果你对“读就绪”的通道感兴趣,select()方法会返回读事件已经就绪的那些通道。
Here are the select()
methods:
- int select()
- int select(long timeout)
- int selectNow()
select() blocks until at least one channel is ready for the events you registered for. 阻塞到至少有一个通道在你注册的事件上就绪了—注意这里会阻塞
select(long timeout) does the same as select()
except it blocks for a maximum of timeout
milliseconds (the parameter). 指定毫秒级超时时间
selectNow() doesn't block at all. It returns immediately with whatever channels are ready.
The int
returned by the select()
methods tells how many channels are ready. — 返回已ready的channel数量
That is, how many channels that became ready since last time you called select()
. If you call select()
and it returns 1 because one channel has become ready, and you call select()
one more time, and one more channel has become ready, it will return 1 again. If you have done nothing with the first channel that was ready, you now have 2 ready channels, but only one channel had become ready between each select()
call.
注意这里的ready,是从上次调用select之后开始算起。
即自上次调用select()方法后有多少通道变成就绪状态。如果调用select()方法,因为有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在第二次select()方法调用时,也只会返回一个通道。
selectedKeys()
Once you have called one of the select()
methods and its return value has indicated that one or more channels are ready, you can access the ready channels via the "selected key set", by calling the selectors selectedKeys()
method.
如果调用select返回的可用channel数目大于等于1,就可以使用selector.selectedKey()方法获取selectionKey的set集合
Here is how that looks:
Set selectedKeys = selector.selectedKeys();
When you register a channel with a Selector
the Channel.register()
method returns a SelectionKey
object. This key represents that channels registration with that selector. It is these keys you can access via the selectedKeySet()
method. From the SelectionKey
.
You can iterate this selected key set to access the ready channels. Here is how that looks:
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();
}
This loop iterates the keys in the selected key set. For each key it tests the key to determine what the channel referenced by the key is ready for.
Notice the keyIterator.remove()
call at the end of each iteration. The Selector
does not remove the SelectionKey
instances from the selected key set itself. You have to do this, when you are done processing the channel. The next time the channel becomes "ready" the Selector
will add it to the selected key set again.
要手动keyIterator.remove()方法将SelectionKey实例从set中清除,因为Selector不会自动清除。
The channel returned by the SelectionKey.channel()
method should be cast to the channel you need to work with, e.g a ServerSocketChannel or SocketChannel etc.
要做强制转换,转换成注册时的channel类型
wakeUp()
A thread that has called the select()
method which is blocked, can be made to leave the select()
method, even if no channels are yet ready. This is done by having a different thread call the Selector.wakeup()
method on the Selector
which the first thread has called select()
on. The thread waiting inside select()
will then return immediately.
如果一个线程调用select方法后变成blocked状态,即使没有通道已经就绪,也有办法让其从select()方法返回
只要让第二个线程在第一个线程调用select()方法的那个对象上调用Selector.wakeup()方法即可。
阻塞在select()方法上的线程会立马返回。
If a different thread calls wakeup()
and no thread is currently blocked inside select()
, the next thread that calls select()
will "wake up" immediately.
如果有其它线程调用了wakeup()方法,但当前没有线程阻塞在select()方法上,下个调用select()方法的线程会立即“醒来(wake up)”。
close()
When you are finished with the Selector
you call its close()
method. This closes the Selector
and invalidates all SelectionKey
instances registered with this Selector
. The channels themselves are not closed.
用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效。通道本身并不会关闭。
Full Selector Example
Here is a full example which opens a Selector
, registers a channel with it (the channel instantiation is left out), and keeps monitoring the Selector
for "readiness" of the four events (accept, connect, read, write).
public static void main(String[] args) throws IOException {
Selector selector = Selector.open(); // 构造selector
SocketChannel socketChannel = SocketChannel.open(); // 构造 socketChannel
socketChannel.validOps();
DatagramChannel udpChannel = DatagramChannel.open(); // 构造 udpChannel
socketChannel.configureBlocking(false); // 设置非阻塞
udpChannel.configureBlocking(false); // 设置非阻塞
// 试试fileChannel,根本没有configureBlocking方法,丢。。。
// RandomAccessFile randomAccessFile = new RandomAccessFile("nio-data.txt","rw");
// FileChannel fileChannel = randomAccessFile.getChannel();
// fileChannel.con....没有这个方法
socketChannel.register(selector, SelectionKey.OP_WRITE); // 不是ServerSocketChannel,不能用OP_ACCEPT状态,否则会报参数非法
udpChannel.register(selector, SelectionKey.OP_READ);
while (true) {
int idleChannelSize = selector.select(2000); // 这里会一直阻塞,所以要加超时时间
System.out.println("now ready channel size:" + idleChannelSize);
if (0 == idleChannelSize) continue;
Set idleChannelSet = selector.selectedKeys();
Iterator keyIterator = idleChannelSet.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
System.out.println("Key is acceptable," + key.channel().toString());
} else if (key.isConnectable()) {
System.out.println("Key is connectable," + key.channel().toString());
} else if (key.isReadable()) {
System.out.println("Key is readable," + key.channel().toString());
} else if (key.isWritable()) {
System.out.println("Key is writeable," + key.channel().toString());
}
keyIterator.remove(); // 在里面这层循环里处理,否则remove谁呢?
}
}
}
这里的两个channel其实都不是ready状态,所以select()方法会一直阻塞。这里后面加入了超时时间,就会打印
now ready channel size:0
now ready channel size:0
now ready channel size:0