1、选择器介绍
选择器的作用是完成IO的多路复用
并不是所有通道都可以被选择器监控或者选择的,例如FileChannel就不能被选择器复用。判断一个通道是否能被选择器监控或者选择只需看其是否继承了抽象类SelectableChannel
选择器和通道是监控和被监控的关系。通过选择器可以监控多个通道的IO情况。
一般来说,一个单线程处理一个选择器,一个选择器可以监控很多通道,通过选择器,一个单线程就可以处理数千数万甚至更多的通道。在极端情况下(数万连接),只用一个线程就可以处理所有的通道,会大量的减少线程之间上下文切换的开销。
单线程可以配合 Selector 完成对多个 Channel 可读写事件的监控,这称之为多路复用
- 多路复用仅针对网络 IO、普通文件 IO 没法利用多路复用
- 如果不用 Selector 的非阻塞模式,线程大部分时间都在做无用功,而 Selector 能够保证
- 有可连接事件时才去连接
- 有可读事件才去读取
- 有可写事件才去写入
- 限于网络传输能力,Channel 未必时时可写,一旦 Channel 可写,会触发 Selector 的可写事件
- 限于网络传输能力,Channel 未必时时可写,一旦 Channel 可写,会触发 Selector 的可写事件
- 有可连接事件时才去连接
通道和选择器的关系通过通道注册的形式确定,调用通道的注册方法可以将通道实例注册到一个选择器中,注册方法有两个参数一个是指定的选择器实例,一个是指定选择器监控的IO事件类型。
- 可读
- 可写
- 连接
- 接收
2、选择器使用
2.1、监听 Channel 事件
可以通过下面三种方法来监听是否有事件发生,方法的返回值代表有多少 channel 发生了事件
方法1,阻塞直到绑定事件发生
int count = selector.select();
方法2,阻塞直到绑定事件发生,或是超时(时间单位为 ms)
int count = selector.select(long timeout);
方法3,不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件
int count = selector.selectNow();
💡 select 何时不阻塞
- 事件发生时
- 客户端发起连接请求,会触发 accept 事件
- 客户端发送数据过来,客户端正常、异常关闭时,都会触发 read 事件,另外如果发送的数据大于 buffer 缓冲区,会触发多次读取事件
- channel 可写,会触发 write 事件
- 在 linux 下 nio bug 发生时
- 调用 selector.wakeup()
- 调用 selector.close()
- selector 所在线程 interrupt
2.2、注册选择器
SelectionKey serverSelectKey = ssc.register(通道, 关注事件, 关联的buffer);
- 可读 SelectionKey.OP_READ
- 可写 SelectionKey.OP_WRITE
- 连接 SelectionKey.OP_CONNECT
- 接收 SelectionKey.OP_ACCEPT
通过或运算监控通道的多个事件:
int ket = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
3、选择器实战
server端
@Slf4j
public class SelectorServer {
public static void main(String[] args) throws IOException {
//1、创建selector 选择器,管理多个channel
Selector selector = Selector.open();
//2、创建服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8989));
//3、注册selector
/**
* SelectionKey为事件发生后,通过它可以知道事件和那个channel发生的事件
* 事件分为:accept(有连接请求时触发),connect(建立连接时触发),read,write
*/
SelectionKey serverSelectKey = ssc.register(selector, 0, null);
//服务端只关注accept
serverSelectKey.interestOps(SelectionKey.OP_ACCEPT);
log.info("register key : {}",serverSelectKey);
while (true){
//3、select 没有事件发生则线程阻塞,在事件未处理时,它不会阻塞,事件处理或者取消就会阻塞
// selector会在事件发生后向selectedKeys集合中加入key,但是不会删除key,所以处理完一个key,需要删除
selector.select();
//4、处理事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()){
SelectionKey key = iterator.next();
//处理key时要从selectedKeys集合删除
iterator.remove();
log.info("key: {}",key);
//5、区分事件类型
if(key.isAcceptable()){
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverChannel.accept();
log.info("accepted... {}",clientChannel);
clientChannel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(1024);
//byteBuffer非线程安全,将byteBuffer作为附件关联到clientSelectKey上
SelectionKey clientSelectKey = clientChannel.register(selector, SelectionKey.OP_READ, buffer);
log.info("clientSelectKey {}",clientSelectKey);
}else if(key.isReadable()){
try {
SocketChannel clientChannel = (SocketChannel) key.channel();
//获取关联的byteBuffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
//如果客户端是正常断开,read返回-1
int read = clientChannel.read(buffer);
if(read == -1){
key.cancel();
log.info("客户端 {} 断开",clientChannel);
}else {
split(buffer);
//扩容
if(buffer.limit() == buffer.position()){
ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
buffer.flip();
newBuffer.put(buffer);
key.attach(newBuffer);
}
}
} catch (IOException e) {
//避免客户端异常断开造成异常导致服务器停掉,需要将key从选择器key集合中删除
e.printStackTrace();
key.cancel();
}
}
}
}
}
private static void split(ByteBuffer source) {
source.flip();
for (int i = 0; i < source.limit() ; i++) {
if (source.get(i) == '#'){
int len = i + 1 - source.position();
ByteBuffer target = ByteBuffer.allocate(len);
for (int j = 0; j < len; j++) {
byte b = source.get();
target.put(b);
}
target.flip();
log.info(StandardCharsets.UTF_8.decode(target).toString());
}
}
source.compact();
}
}
💡 事件发生后能否不处理
事件发生后,要么处理,要么取消(cancel),不能什么都不做,否则下次该事件仍会触发,这是因为 nio 底层使用的是水平触发
💡 为何要 iter.remove()
因为 select 在事件发生后,就会将相关的 key 放入 selectedKeys 集合,但不会在处理完后从 selectedKeys 集合中移除,需要我们自己编码删除。例如
- 第一次触发了 ssckey 上的 accept 事件,没有移除 ssckey
- 第二次触发了 sckey 上的 read 事件,但这时 selectedKeys 中还有上次的 ssckey ,在处理时因为没有真正的 serverSocket 连上了,就会导致空指针异常
💡 cancel 的作用
cancel 会取消注册在 selector 上的 channel,并从 keys 集合中删除 key 后续不会再监听事件
💡 客户端断开事件
如果客户端是正常断开,read返回-1 客户端异常断开会抛出IOException异常
client端
public class Client {
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost",8989));
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String str = scanner.next();
if("close".equals(str)){
sc.close();
}else {
sc.write(StandardCharsets.UTF_8.encode(str));
}
}
}
}