今天来学习NIO中另一个重要的知识,选择器。
NIO这本书讲解的呢,说实话有点乱,感觉就是在翻译,乱的让人看不懂很多东西,所以这篇文章结合了一些博客去理解。
在选择器章节中,有三个很关键的角色,也是核心类,SelectableChannel,SelectionKey和Selector。
1.SelectableChannel
SelectableChannel是可选择通道,它可以被注册到Select对象上,同时可以指定对那个选择器而言,哪种操作是感兴趣的。一个通道可以被注册到多个选择器上,但是对于每个选择器而言只能被注册一次。
Channel分为两大块:
1. FileChannel,主要针对文件的IO的Channel,但是它不支持非阻塞模式,因此它不能支持选择。
2. SelectableChannel,除此之外的Channel,比如Soeket IO就是支持非阻塞模式的。
1.1 设置非阻塞模式
我们可以通过设置它的阻塞模式以便于支持选择:
public abstract SelectableChannel configureBlocking(boolean block) throws IOException;
1.2 注册
上图中Channel和Selector是需要通过SelectableChannel关联的,关联的方法就是register方法:
public abstract SelectionKey register(Selector sel, int ops, Object att)throws ClosedChannelException;
public final SelectionKey register(Selector sel, int ops)throws ClosedChannelException {
return register(sel, ops, null);
}
2.SelectionKey
在register方法中的第二个参数表示的是感兴趣事件,是一个"insterest"集合,定义在SelectionKey中。可以监听4种不同类型的事件:
1. Connect(连接) ---> OP_CONNECT(1 << 3)
2. Accept(接受) ---> OP_ACCEPT(1 << 4)
3. Read(读) ---> OP_READ(1 << 0)
4. Write(写) ---> OP_WRITE(1 << 2)
Selectkey是关联对象,关联对象内包含了比较多的信息:
1. interest集合
2. read集合
3. Channel对象
4. Selector对象
5. 附加对象(可选)
1. interest集合
这个集合是在注册通道时选择的感兴趣的事件集合。
public abstract int interestOps();
public abstract SelectionKey interestOps(int ops);
2. ready集合
ready集合是通道已经准备就绪的操作的集合。
public abstract int readyOps();
可以通过一些方法来判断Channel中什么事件已经就绪:
public final boolean isAcceptable() {
return (readyOps() & OP_ACCEPT) != 0;
}
public final boolean isConnectable() {
return (readyOps() & OP_CONNECT) != 0;
}
public final boolean isReadable() {
return (readyOps() & OP_READ) != 0;
}
public final boolean isWritable() {
return (readyOps() & OP_WRITE) != 0;
}
3&&4. Channel和Selector
public abstract SelectableChannel channel();
public abstract Selector selector();
5. 附加对象
SelectableChannel提供你可以添加更多的对象信息到Select中,通过register方法可以传入,并且后续你可以得到附加对象。
3.Selector
3.1 Selector创建
Selector是真正发挥作用的对象。通常是由静态方法open实例化出来。内部其实调用的是SelectorProvider对象的openSelector方法,也可以直接调用它。使用结束后close方法释放资源。
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
3.2 Selector选择
一旦Selector注册了通道后,可以调用select()方法来返回感兴趣事件或者已经准备就绪的通道。
int select()
int select(long timeout)
int selectNow()
1. select() 阻塞到至少有一个通道在你注册的事件上就绪了。
2. select(timeout) 增加了阻塞的时间。
3. selectNow() 不会阻塞(如果自从前一次选择操作后,没有通道变成可选择的,直接返回0)。
select()方法返回本次执行select时从未准备好到准备好状态的Channel数。如果返回不为0,那么可以执行下面的操作:
public abstract Set selectedKeys( );
这个方法就能获取到一个包含SelectionKey对象的集合,分别对应各个准备好的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();
}
遍历循环可以检测每个键对应的通道的就绪事件。
最后的移除操作必须服务端实现,Selector不会参与。
3.3 Selector唤醒wakeUp
由于select()方法是阻塞的,因此当没有通道就绪时,怎么让该线程返回呢?可以调用wakeUp()方法。具体操作如下:
1. 其他线程在调用select()方法的对象上调用Selector.wakeup()。阻塞的线程会立马返回。
2. 另外如果调用wakeup()时没有线程阻塞,那么此后如果有线程调用select()方法,会立马返回。
4.完整示例
int port = 30;
ServerSocketChannel serverChannel = ServerSocketChannel.open( );
ServerSocket serverSocket = serverChannel.socket( );
Selector selector = Selector.open( );
serverSocket.bind (new InetSocketAddress (port));
serverChannel.configureBlocking (false);
serverChannel.register (selector, SelectionKey.OP_ACCEPT);
while (true) {
int n = selector.select( );
if (n == 0) {
continue; // nothing to do
}
Iterator it = selector.selectedKeys().iterator( );
while (it.hasNext( )) {
SelectionKey key = (SelectionKey) it.next( );
if (key.isAcceptable( )) {
ServerSocketChannel server =
(ServerSocketChannel) key.channel( );
SocketChannel channel = server.accept( );
if (channel == null) {
;//handle code, could happen
}
channel.configureBlocking (false);
channel.register (selector, SelectionKey.OP_READ);SelectionKey.OP_READ);
}
if (key.isReadable( )) {
readDataFromSocket (key);
}
it.remove( );
}
}