NIO——选择器
Selector用于将一组Channel的事件发生聚合在一个(确切的说不是一个,但不妨先这样理解)线程中进行处理,还是先介绍它相关组件的抽象结构。
通道和选择器
通道和选择器的关系是,通道注册一部分事件到选择器中,然后用户可以通过选择器对其中所有的通道注册事件进行监听,事件发生,选择器就会接收到。
选择器和通道配合,主要为了提供非阻塞IO,传统的阻塞IO为了避免由于IO等待引起的CPU时间浪费都采用多线程技术,一个线程处理一个任务。但这只是把阻塞移到其他线程中了,一个线程的阻塞不会影响其它线程的正常运行,但是阻塞还是在,这个线程还是在白白等待IO事件完成。非阻塞IO不再需要等待IO事件发生,调用非阻塞IO操作后,立即得到返回,等到对应的事件发生,程序会得到通知。
选择器的作用就是对正在进行的非阻塞IO事件的发生进行监听。
SelectableChannel
一个事实是,不是也不需要所有通道都可以注册到选择器中,比如FileChannel
就不行,因为文件操作在本地,通常比较快,不会有太多的CPU闲置情况。可以注册到选择器中的通道是一个SelectableChannel
实例。
public abstract class SelectableChannel
extends AbstractInterruptibleChannel
implements Channel
SelectableChannel
继承AbstractInterruptibleChannel
,并提供了一些新的和选择器相关的功能,比如如下方法把自己注册到一个Selector
中
public abstract SelectionKey register(Selector sel, int ops, Object att)
throws ClosedChannelException;
SelectableChannel
的子类如下:
isRegistered
方法返回该Channel当前是否注册到任何一个Selector中
public abstract boolean isRegistered();
不能直接通过SelectableChannel
或者Selector
取消二者的注册关系,而是要通过SelectionKey
的cancel
方法取消,调用后将会在选择器的下一次select
时注销。
如果通过调用通道的close
或者中断阻塞在该通道上IO操作中的线程来关闭该通道,都会隐式取消该通道的所有SelectionKey
。而如果选择器关闭了,上面的通道和SelectionKey立即无效。
一个通道对于一个特定选择器只能注册一次。
SelectableChannel
默认属于阻塞模式,如果使用Selector
,必须通过configureBlocking(false)
来设置非阻塞模式。
AbstractSelectableChannel
public abstract class AbstractSelectableChannel
extends SelectableChannel
维护通道的阻塞模式和SelectionKey集合,实现SelectableChannel接口的所有约定。
ServerSocketChannel和NetworkChannel
ServerSocketChannel
继承AbstractSelectableChannel
并且实现NetworkChannel
public abstract class ServerSocketChannel
extends AbstractSelectableChannel
implements NetworkChannel
ServerSocketChannel
并不包含一个套接字实现,而是通过它的socket
方法返回一个ServerSocket
对象,然后对该对象进行设置。不能通过ServerSocket
来创建通道,不能指定与ServerSocketChannel
关联的ServerSocket
对象。
通过ServerSocketChannel
的open
方法可以创建一个该类的实例,你需要通过获取它其中的socket
并进行绑定。
下面是该类所有的方法
ServerSocketChannel、Selector和SelectionKey的使用
@Test
void testSelectableChannel() throws IOException {
Selector selector = Selector.open();
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.socket().bind(new InetSocketAddress("0.0.0.0", 11441));
SelectionKey key = ssChannel.register(selector, SelectionKey.OP_ACCEPT);
Assertions.assertTrue(ssChannel.isRegistered());
System.out.printf("selector: %s\nssChannel: %s\nkey: %s\n", selector, ssChannel, key);
ssChannel.close();
}
SelectorProvider
Selector可以向系统注册Channel的IO事件通知并在事件发生时接到通知。这个功能需要操作系统的支持,而且不同操作系统中的实现方式是不一样的。所以SelectorProvider用来提供一个Selector,屏蔽底层带来的差异,Selector.open
方法使用的就是这个。
Selector的使用
Selector维护了三种键集
- 键集:每个注册到其中的通道都会有一个绑定的
SelectionKey
对象,键集返回所有这些对象,此集合由keys
方法返回 - 已选择键集:一个通道可能在一个选择器上监听多种事件,但一个通道只绑定一个
SelectionKey
对象,已选择键集代表至少该通道在该选择器上有一个准备就绪的事件。selectedKeys
方法返回这些键集。已选择键集是所有键集的一个子集。 - 已取消键集:已被取消但是通道尚未注销的键集合,是所有键集的自己,不可访问,并且在
select
方法执行期间会将已取消的键从所有键集中移除。
select
、select(long)
和selectNow
方法执行涉及以下三个步骤:
- 将所有已取消键集从所有键集中移除,并注销其通道
- 通过操作系统来获得通道的准备就绪信息
- 如果该通道的键尚未在就绪键集中,添加到就绪键集中,并检查就绪键集,丢弃以往过期的就绪键
- 如果在步骤2进行时将任何键添加到已取消键集,按步骤1进行处理
select
操作是阻塞的,直到键集中有正在等待的事件发生,该阻塞可以通过以下手段中断:
- 调用
Selector.wakeup
- 调用
Selector.close
select返回值的含义
select
返回已更新为就绪键的键的数目,所以它可能是0,并且它是0不代表没有事件可以处理。因为可能同一个事件连续发生,一直在就绪键集中没有被清出。
@Test
void testSelectReturn0() throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress("localhost", 11441));
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
int nAddToSelectedSet = selector.select();
int selectedKeysCount = selector.selectedKeys().size();
System.out.printf("nAddToSelectedSet => %d, selectedKeysCount: %d\n", nAddToSelectedSet, selectedKeysCount);
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
keys.next();
ssc.accept();
// 注释这行,导致该键一直在就绪键集中
// keys.remove();
}
}
}
下面随便运行点什么,两次连接到这个端口:
nAddToSelectedSet => 1, selectedKeysCount: 1
nAddToSelectedSet => 0, selectedKeysCount: 1
第二次select
返回0。
读写文件的例子
先运行server
,再运行client
,当然classpath中要有个文件
public class TransferFileTest {
@Test
void server() throws IOException, URISyntaxException {
ServerSocketChannel channel = ServerSocketChannel.open();
channel.configureBlocking(false);
channel.bind(new InetSocketAddress("localhost", 11441));
Selector selector = Selector.open();
channel.register(selector, SelectionKey.OP_ACCEPT);
boolean isRun = true;
while (isRun) {
selector.select();
Set<SelectionKey> set = selector.selectedKeys();
Iterator<SelectionKey> iterator = set.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
SocketChannel connChannel = channel.accept();
connChannel.configureBlocking(false);
connChannel.register(selector, SelectionKey.OP_WRITE);
}
if (key.isWritable()) {
System.out.println("开始写入");
FileChannel fc = FileChannel.open(
Path.of(
getClass().getResource("../../../../sc.png").toURI()
),
StandardOpenOption.READ
);
SocketChannel connChannel = (SocketChannel) key.channel();
fc.transferTo(0, fc.size(), connChannel);
System.out.println("完成写入");
fc.close();
connChannel.close();
}
}
}
channel.close();
}
@Test
void client() throws IOException {
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.connect(new InetSocketAddress("localhost", 11441));
Selector selector = Selector.open();
channel.register(selector, SelectionKey.OP_CONNECT);
boolean isRun = true;
while (isRun) {
selector.select();
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
keys.remove();
if (key.isConnectable()) {
while (!channel.finishConnect()) {}
channel.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
FileChannel fc = FileChannel.open(
Path.of("D:\\tmp\\sc-cp.png"),
StandardOpenOption.CREATE, StandardOpenOption.WRITE
);
ByteBuffer buf = ByteBuffer.allocate(1024);
System.out.println("开始读取");
while (channel.read(buf) != -1) {
buf.flip();
fc.write(buf);
buf.clear();
}
System.out.println("读取完毕");
fc.close();
channel.close();
break;
}
}
}
}
}
wakeup
使尚未返回的第一个选择操作立即返回,如果当前没有尚未返回的选择操作,之后的第一次选择操作将立即返回。
@Test
void testWakeup() throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress("localhost", 11441));
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
new Thread(() -> {
try {
Thread.sleep(3000);
selector.wakeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
int n = selector.select();
System.out.println("select返回 n is " + n);
selector.close();
ssc.close();
}
若干需要注意的细节
SelectionKey.cancel()
取消后,该通道将在下一次select
时取消注册- 通道
close
后,下一次select
时取消该通道的注册 - 新创建的选择器中三个键集都是空的
- 尝试删除键集中的键会导致
UnsupportedOperationException
- 多线程环境下删除键集中的键会导致
ConcurrentModificationException
- 阻塞在
select
中的线程在选择器close
时会被中断,并抛出ClosedSelectorException
- 阻塞在
select
的线程在interrupt
时,select
会正常返回,你可以通过interrupted
方法清除中断状态 Selector.close
删除全部键,注销全部通道
第八点的测试
@Test
void testInterrupt() throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress("localhost", 11441));
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
Thread selectorThread = Thread.currentThread();
new Thread(() -> {
try {
Thread.sleep(3000);
selectorThread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
int n = selector.select();
System.out.println("select返回 n is " + n);
selector.close();
ssc.close();
}
SelectionKey
代表一个通道和一个选择器的绑定。
在调用某个SelectionKey的cancel方法、close通道、close选择器,该通道与选择器的绑定一直有效。
SelectionKey包含两个集合,都是整数表示的操作集,每一位都表示该通道支持的一类可选操作
- interest集:选择器
select
关心的操作集合。创建键时使用给定的值初始化该集合,之后可以通过interestOps(int)
方法对其进行更改 - ready集:
interest
集中那些已就绪的操作的集合,初始为0,select
方法中选择器会对它进行更新,不能手动更新
SelectionKey定义了所有已知的操作集位,可以通过通道的validOps
方法判断是否支持某个操作集位。
四种测试方法
isAcceptable
:测试此键的通道是否已经准备接收新的套接字连接isConnectable
:测试此键的通道是否已经完成其套接字连接操作isReadable
:测试此键的通道是否已经准备好读取isWritable
:测试此键的通道是否已经准备好写入
channel
返回创建此键的通道
selector
返回关联的选择器
附件
attach
方法设置SelectionKey的附件,attachment
方法返回设置的附件
这是书上的一个小例子,可能是为了方便吧,还没太理解。
interestOps
该方法可以设置SelectionKey关注的操作位集
DatagramChannel
略