一、Selector
简介
1. Selector
和 Channel
关系
- Selector 选择器,也可以翻译为 多路复用器 。
- 它是 Java NIO 核心组件中的一个。
- 用于检查一个或多个 NIO Channel(通道)的状态,是否处于可读、可写。
- 如此可以实现单线程管理多个 Channel,也就是可以管理多个网络链接。
- 使用 Selector 的好处。
- 使用更少的线程就可以来处理通道了。
- 相比使用多个线程,避免了线程上下文切换带来的开销。
2. SelectableChannel
可选择通道
- 不是所有的 Channel 都可以被 Selector 复用的。
- FileChannel 就不能被选择器复用。
- 判断一个 Channel 能被 Selector 复用,前提是判断他是否继承了一个抽象类
SelectableChannel
。- 如果继承了 SelectableChannel,则可以被复用,否则不能。
- SelectableChannel 类提供了实现通道的可选择性,所需要的公共方法。
- 它是所有支持就绪检查的通道类的父类。
- 所有 Socket 通道,都继承了 SelectableChannel 类都是可选择的,包括从管道(Pipe)对象的中获得的通道。
- 而 FileChannel 类,没有继承 SelectableChannel,因此是不是可选通道。
- 一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。
- 通道和选择器之间的关系,使用注册的方式完成。
- SelectableChannel 可以被注册到 Selector 对象上,在注册的时候,需要指定通道的哪些操作,是 Selector 感兴趣的。
3. Channel
注册到 Selector
channel.register(Selector sel, int ops);
方法,将一个通道注册到一个选择器。
- 第一个参数,指定通道要注册的选择器。
- 第二个参数,指定选择器需要查询的通道操作。
- 供选择器查询的通道操作(从类型来分):
SelectionKey.OP_READ
可读。SelectionKey.OP_WRITE
可写。SelectionKey.OP_CONNECT
连接。SelectionKey.OP_ACCEPT
接收。
- 如果 Selector 对通道的多操作类型感兴趣,可以用 位或 操作符实现。
// 可读可写
int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
- 选择器查询的不是通道的操作,而是通道的某个操作的一种就绪状态。
- 什么是操作的就绪状态?
一旦通道具备完成某个操作的条件,表示该通道的某个操作已经就绪,就可以被 Selector 查询到,程序可以对通道进行对应的操作。- 一个 SocketChannel 通道可以连接到一个服务器,则处于 连接就绪(OP_CONNECT)。
- 一个 ServerSocketChannel 服务器通道准备好接收新进入的连接,则处于 接收就绪(OP_ACCEPT) 状态。
- 一个有数据可读的通道,可以说是 读就绪(OP_READ)。
- 一个等待写数据的通道,可以说是 写就绪(OP_WRITE)。
4. SelectionKey
选择键
- Channel 注册后,并且一旦通道处于某种就绪状态,就可以被选择器查询到。
- 使用选择器(Selector)的 select() 方法完成。
- select() 方法的作用,对感兴趣的通道操作,进行就绪状态的查询。
- Selector 可以不断的查询 Channel 中发生操作的就绪状态。并且挑选感兴趣的操作就绪状态。
- 一旦通道有操作的就绪状态达成,并且是 Selector 感兴趣的操作,就会被 Selector 选中,放入选择键集合中。
- 一个选择键。
- 首先是包含了注册在 Selector 的通道操作的类型。
比方说SelectionKey.OP_READ
可读。- 也包含了 特定的通道 与 特定的选择器 之间的注册关系。
- 开发应用程序时,选择键是编程的关键。
- NIO 的编程,就是根据对应的选择键,进行不同的业务逻辑处理。
- 选择键的概念 和 事件的概念比较相似。
- 一个选择键类似监听器模式里边的一个事件。
- 由于 Selector 不是事件触发的模式,而是主动去查询的模式,所以不叫事件 Event,而是叫 SelectionKey 选择键。
5. Selector
示例
/**
* @author: wy
* describe: Selector 示例
* 1. Selector 创建
* 2. Channel 注册到 Selector
* 3. 轮询查询就绪操作
*/
public class Selector1 {
public static void main(String[] args) throws IOException {
// 一、获取通道
ServerSocketChannel channel = ServerSocketChannel.open();
// 1. 绑定连接
channel.bind(new InetSocketAddress(9999));
// 2. 设置为非阻塞
channel.configureBlocking(false);
// 二、获取 Selector 选择器
Selector selector = Selector.open();
/*
三、将通道注册到选择器上,并指定监听事件为: 接收事件
1. Channel 必须处于非阻塞模式下,否则将抛出异常 IllegalBlockingModeException。
*/
channel.register(selector, SelectionKey.OP_ACCEPT);
// 四、查询已经就绪通道操作
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 1. 遍历集合
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 2. 判断key就绪状态操作
if (key.isAcceptable()) {
// 3. ServerSocketChannel 已接受连接
} else if (key.isConnectable()) {
// 4. 已与远程服务器建立连接
} else if (key.isReadable()) {
// 5. 通道已准备好读取
} else if (key.isWritable()) {
// 6. 通道已准备好写入
}
iterator.remove();
}
}
}
5.1 Selector
创建
// 二、获取 Selector 选择器
Selector selector = Selector.open();
5.2 Channel
注册到 Selector
- 实现 Selector 管理 Channel。
- 需要将 Channel 注册到相应的 Selector 上。
// 一、获取通道
ServerSocketChannel channel = ServerSocketChannel.open();
// 1. 绑定连接
channel.bind(new InetSocketAddress(9999));
// 2. 设置为非阻塞
channel.configureBlocking(false);
// 二、获取 Selector 选择器
Selector selector = Selector.open();
/*
三、将通道注册到选择器上,并指定监听事件为: 接收事件
1. Channel 必须处于非阻塞模式下,否则将抛出异常 IllegalBlockingModeException。
*/
channel.register(selector, SelectionKey.OP_ACCEPT);
- 注意:
- 与 Selector 一起使用时,Channel 必须处于非阻塞模式下,否则将抛出异常 IllegalBlockingModeException。
- 意味着 FileChannel 不能与 Selector 一起使用,因为 FileChannel 不能切换到非阻塞模式,而套接字相关的所有的通道都可以。
- 一个通道,并没有一定要支持所有的四种操作。
- 比如服务器通道 ServerSocketChannel 支持 Accept 接受操作。
- 而 SocketChannel 客户端通道则不支持。
- 可以通过通道上的
validOps()
方法,来获取特定通道下所有支持的操作集合。
5.3 轮询查询就绪操作
- 通过 Selector 的 select() 方法,可以查询出已经就绪的通道操作。
- 这些就绪的状态集合,存在一个
Set<SelectionKey>
集合中。
Selector.select()
几个重载的查询方法:
select()
:阻塞到至少有一个通道在注册的事件上就绪了。select(long timeout)
:和 select() 一样,但最长阻塞时间为timeout
毫秒。selectNow()
:非阻塞,只要有通道就绪就立刻返回。
- select() 方法,返回 int 值,表示有多少通道已经就绪。
- 准确的说,是自前一次 select() 方法到这一次 select() 方法之间的时间段,有多少通道变成就绪状态。
- 如:首次调用 select() 方法,如果有一个通道变成就绪状态,返回了 1。
- 再次调用 select() 方法,如果另一个通道就绪了,会再次返回 1。
- 如果对第一个就绪的 Channel 没有做任何操作,现在就有两个就绪的通道。
- 但在每次 select() 方法调用之间,只有一个通道就绪了。
- select() 方法,返回值不为 0 时。
- 在 Selector 中有一个 selectedKeys() 方法,用来访问已选择键集合。
- 迭代集合的每一个选择键元素,根据就绪操作的类型,完成对应的操作。
// 四、查询已经就绪通道操作
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 1. 遍历集合
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 2. 判断key就绪状态操作
if (key.isAcceptable()) {
// 3. ServerSocketChannel 已接受连接
} else if (key.isConnectable()) {
// 4. 已与远程服务器建立连接
} else if (key.isReadable()) {
// 5. 通道已准备好读取
} else if (key.isWritable()) {
// 6. 通道已准备好写入
}
iterator.remove();
}
6. 唤醒 select()
方法
- 选择器执行选择的过程,系统底层会依次询问每个通道是否已经就绪。
- 这个过程,可能会造成调用线程进入阻塞状态。
- 下面两个方法,可以唤醒在 select() 方法中阻塞的线程。
wakeup()
方法。
- 通过调用 Selector 对象的 wakeup() 方法,让处在阻塞状态的 select() 方法立刻返回。
- 该方法使得选择器上的第一个还没有返回的选择操作立即返回。
- 如果当前没有进行中的选择操作,那么下一次对 select() 方法的一次调用将立即返回。
close()
方法。
- 通过 close() 方法关闭 Selector。
- 该方法使得任何一个在选择操作中阻塞的线程,都被唤醒(类似:wakeup() 方法)。
- 同时使得注册到该 Selector 的所有 Channel 被注销,所有的键将被取消。
- 但是 Channel 本身并不会关闭。
7. 服务端示例
/**
* 1. 服务端示例
*/
@Test
public void server() throws IOException {
// 一、获取服务端通道
ServerSocketChannel channel = ServerSocketChannel.open();
// 1. 绑定端口号
channel.bind(new InetSocketAddress(8080));
// 2. 切换到非阻塞模式
channel.configureBlocking(false);
// 二、获取 Selector 选择器
Selector selector = Selector.open();
/*
三、将通道注册到选择器上,并指定监听事件为: 接收事件
1. Channel 必须处于非阻塞模式下,否则将抛出异常 IllegalBlockingModeException。
*/
channel.register(selector, SelectionKey.OP_ACCEPT);
// 四、创建 Buffer,添加数据
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
ByteBuffer writeBuffer = ByteBuffer.allocate(128);
writeBuffer.put("服务端".getBytes());
writeBuffer.flip();
System.out.println("服务端已启动...");
// 五、选择器进行轮询,进行后续操作
int ready = 0;
while ((ready = selector.select()) > 0) {
// 1. 查询已经就绪通道操作
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 2. 遍历集合
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
// 3. 获取就绪操作
SelectionKey key = iterator.next();
// 4. 判断key就绪状态操作
if (key.isAcceptable()) {
// 5. ServerSocketChannel 已接受连接
// 创建新的连接
SocketChannel socketChannel = channel.accept();
// 切换到非阻塞模式
socketChannel.configureBlocking(false);
// 把连接注册到 Selector 上,声明这个 Channel 只对读操作感兴趣
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.printf("%s, 注册成功!", socketChannel.getLocalAddress()).println();
} else if (key.isReadable()) {
// 6. 通道已准备好读取
SocketChannel socketChannel = (SocketChannel) key.channel();
// 读取数据
int length = 0;
while ((length = socketChannel.read(readBuffer)) > 0) {
readBuffer.flip();
System.out.printf("%s: %s", socketChannel.getRemoteAddress(), new String(readBuffer.array(), 0, length)).println();
readBuffer.clear();
}
key.interestOps(SelectionKey.OP_WRITE);
} else if (key.isWritable()) {
// 7. 通道已准备好写入
writeBuffer.rewind();
SocketChannel socketChannel = (SocketChannel) key.channel();
socketChannel.write(writeBuffer);
key.interestOps(SelectionKey.OP_READ);
}
iterator.remove();
}
}
System.out.println("服务端结束!");
}
8. 客户端示例
/**
* 2. 客户端示例
*/
public static void main(String[] args) throws IOException {
// 一、获取通道,绑定主机和端口号
SocketChannel channel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8080));
// 1. 切换到非阻塞模式
channel.configureBlocking(false);
// 2. 创建 Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
System.out.println("客户端已启动...");
Scanner selector = new Scanner(System.in);
while (selector.hasNext()) {
String next = selector.next();
String date = DateFormat.getDateInstance().format(new Date());
// 1. 添加数据
buffer.put(String.format("date: %s, str: %s", date, next).getBytes());
// 2. 切换模式
buffer.flip();
// 3. 倒回
buffer.rewind();
// 4. 写入通道
channel.write(buffer);
// 5. 清除
buffer.clear();
// 6. 读取
int read = channel.read(buffer);
System.out.printf("read: %s", read).println();
}
}
二、NIO 编程步骤总结
第一步:创建 Selector 选择器。
第二步:创建 ServerSocketChannel 通道,并绑定监听端口。
第三步:设置 Channel 通道是非阻塞模式。
第四步:把 Channel 注册到 Socketor 选择器上,监听连接事件。
第五步:调用 Selector 的 select() 方法(循环调用),监测通道的就绪状况。
第六步:调用 selectKeys() 方法获取就绪 Channel 集合。
第七步:遍历就绪 Channel 集合,判断就绪事件类型,实现具体的业务操作。
第八步:根据业务决定是否需要再次注册监听事件,重复执行第三步操作。