前言
在之前文章1. Java NIO三大部件概述中简单提及Selector概念、优缺点以及Netty中对其的优化,在这篇文章将对Selector的使用方法进行简单介绍。之后还会有关于Selector如何实现进行概括。
1. Selector创建
Selector selector = Selector.open();
2. 注册Channel到Selector
Selector要能够管理Channel,需要将Channel注册到Selector中:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
这里需注意,如果Channel要注册到Selector中,Channel必须是非阻塞模式。 所以channel.configureBlocking(false);
所以,FileChannel不适用于Selector,因为他是阻塞的。(FileChannel没有继承SelectableChannel)。
SelectableChannel抽象类有一个configureBlocking()方法用于使通道处于阻塞模式或者非阻塞模式。
abstract SelectableChannel configureBlocking(boolean block)
SelectableChannel抽象类的configureBlocking()方法是由AbstractSelectableChannel抽象类实现的,SocketChannel、ServerSocketChannel、DatagramChannel都是直接继承了AbstractSelectableChannel抽象类。
在 register(Selector selector, int interestSet)方法的第二个参数,表示一个“interest”集合,意思是通过Selector监听Channel时,对哪些(可以为多个)事件感兴趣,可以监听四种不同类型的事件:
- Connect:连接完成事件(TCP连接),仅适用于客户端,对应SelectionKey.OP_CONNECT。
- Accept:接受新连接事件,仅适用于服务端,对应SelectionKey.OP_ACCEPT.
- Read:读事件,适用于客户端、服务端,对应SelectionKey.OP_READ,表示Buffer可读。
- Write:写事件,适用于客户端、服务端,对应SelectionKey.OP_WRITE,表示Buffer可写。
Channel触发了一个事件,意思是该事件已经就绪。
- 一个Client Channel Channel成功连接到另一个服务器,称为“连接就绪”。
- 一个Server Socket Channel准备好接收新进入的连接,称为“接受就绪”。
- 一个有数据可读的Channel,可以说是“读就绪”。
- 一个等待写数据的Channel,可以说是“写就绪”。
这里由于Selector可以对Channel的多个事件感兴趣,所以当我们想要注册Channel的多个事件到Selector中时,可用|来组合多个事件:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
这里在实际操作时,Selector对Channel感兴趣的事件集合可通过调用register(Selector selector, int interestSet)方法进行更改:
channel.register(selector, SelectionKey.OP_READ);
channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
3. SelectionKey类
当调用Channel的register(…)方法时,向Selector注册一个Channel后,会返回一个SelectionKey对象,SelectionKey在java.nio.channels包下,被定义成一个抽象类,表示一个Channel和一个Selector的注册关系,包含以下内容:
- interest set : 感兴趣的事件集合
- ready set: 就绪的事件集合
- Channel
- Selector
- attachment:可选择的附加对象
key.interestOps(); //返回代表需要Selector监控的IO操作的bit mask;
key.readyOps(); //返回bit mask,代表在相应channel上可以进行的IO操作。
key.channel(); //返回该SelectionKey对应的channel;
key.selector(): //返回该SelectionKey对应的Selector。
key.attachment(); //返回SelectionKey的attachment,attachment可以在注册channel时被指定。
3.1 interest set
通过调用interestOps()方法,返回感兴趣的事件集合:
int interestSet = selectionKey.interestOps();
//判断对哪些事件感兴趣
boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
其中每个事件Key在SelectionKey中枚举,通过bit表示:
// SelectionKey.java
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
因此,上面的上代码段才可用&运算来判断是否对指定事件感兴趣。
3.2 ready set
通过调用readyOps()方法,返回就绪的事件集合:
//创建ready集合的方法
int readySet = selectionKey.readyOps();
// 判断哪些事件已就绪
//是否可接收,是返回 true
selectionKey.isAcceptable();
//是否可连接,是返回 true
selectionKey.isConnectable();
//是否可读,是返回 true
selectionKey.isReadable();
//是否可写,是返回 true
selectionKey.isWritable();
与interest set相比,ready set 内置了判断事件的方法:
// SelectionKey.java
public final boolean isReadable() {
return (readyOps() & OP_READ) != 0;
}
public final boolean isWritable() {
return (readyOps() & OP_WRITE) != 0;
}
public final boolean isConnectable() {
return (readyOps() & OP_CONNECT) != 0;
}
public final boolean isAcceptable() {
return (readyOps() & OP_ACCEPT) != 0;
}
从SelectionKey访问Channel和Selector的操作如下:
Channel channel = key.channel();
Selector selector = key.selector();
key.attachment();
可以在selectionKey附加一个Object对象,来标识channel对象以便找出你要的channel对象,或者附加一些其他的信息
key.attach(theObject);
Object attachedObj = key.attachment();
也可用register()方法向Selector注册Channel时附加对象:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
3.3 attachment
通过调用attach(Object ob)方法,可以向SelectionKey添加附加对象,调用attachment()方法,可以获得SelectionKey附加对象:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
又获得在注册时,直接添加附加对象:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
4. 通过Selector选择Channel
在Selector中,提供三种类型的Select方法,返回当前有感兴趣事件准备就绪的Channel数量:
// Selector.java
// 阻塞到至少有一个 Channel 在你注册的事件上就绪了。
public abstract int select() throws IOException;
// 在 `select()` 方法的基础上,增加超时机制。最长的阻塞时间为timeout毫秒
public abstract int select(long timeout) throws IOException;
// 和 `select()` 方法不同,立即返回数量,而不阻塞。只要有通道就绪就立刻返回
public abstract int selectNow() throws IOException;
select()方法返回的int值表示有多少通道已经就绪,是自上次调用select()方法后有多少通道变成就绪状态。之前在select()调用时进入就绪的通道不会在本次调用中被记入,而在前一次select()调用进入就绪但现在已经不在处于就绪的通道也不会被记入。但在每次 select 方法调用之间,只有一个 Channel 就绪了,所以才返回 1。
5. 获取可操作的Channel
一旦调用select()方法,并且返回值不为0时,则 可以通过调用Selector的selectedKeys()方法来访问已选择键集合
Set selectedKeys=selector.selectedKeys();
6. 唤醒Selector选择
若某个线程调用select()方法后,发生阻塞,没有通道已就绪,仍有办法让该线程从selector()方法返回:
只要让其他线程在第一个线程调用select()方法的那个 Selector 对象上,调用该 Selector 的 wakeup() 方法,进行唤醒该 Selector 即可。
然后,阻塞在select()方法上的线程会立马返回。(Selector 的 select(long timeout) 方法,若未超时的情况下,也可以满足上述方式。)
如果有其它线程调用了 wakeup() 方法,但当前没有线程阻塞在 select() 方法上,下个调用 select() 方法的线程会立即被唤醒。
7. 关闭Selector
调用Selector的close()方法
该方法使得任何一个在选择操作中阻塞的线程都被唤醒(类似wakeup()),同时使得注册到该Selector的所有Channel被注销,所有的键将被取消,但是Channel本身并不会关闭。(与Selector相关的所有SelectionKey全部失效,与其相关的Channel不会关闭。)
8. Selector示例
服务器端
package com.mec.Test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class WebServer {
public static void main(String[] args) {
try {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8000));
ssc.configureBlocking(false);
Selector selector = Selector.open();
// 注册 channel,并且指定感兴趣的事件是 Accept
ssc.register(selector, SelectionKey.OP_ACCEPT);
ByteBuffer readBuff = ByteBuffer.allocate(1024);
ByteBuffer writeBuff = ByteBuffer.allocate(128);
writeBuff.put("received".getBytes());
writeBuff.flip();
while (true) {
int nReady = selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
if (key.isAcceptable()) {
// 创建新的连接,并且把连接注册到selector上,而且,
// 声明这个channel只对读操作感兴趣。
SocketChannel socketChannel = ssc.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
else if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
readBuff.clear();
socketChannel.read(readBuff);
readBuff.flip();
System.out.println("received : " + new String(readBuff.array()));
key.interestOps(SelectionKey.OP_WRITE);
}
else if (key.isWritable()) {
writeBuff.rewind();
SocketChannel socketChannel = (SocketChannel) key.channel();
socketChannel.write(writeBuff);
key.interestOps(SelectionKey.OP_READ);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端
package com.mec.Test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class WebClient {
public static void main(String[] args) throws IOException {
try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000));
ByteBuffer writeBuffer = ByteBuffer.allocate(32);
ByteBuffer readBuffer = ByteBuffer.allocate(32);
writeBuffer.put("hello".getBytes());
writeBuffer.flip();
while (true) {
writeBuffer.rewind();
socketChannel.write(writeBuffer);
readBuffer.clear();
socketChannel.read(readBuffer);
}
} catch (IOException e) {
}
}
}
运行服务端不断收到客户端消息: