NIO学记:四、选择器Selector

Selector 介绍

Selector 一般称为选择器, 也可以翻译为多路复用器
它是 Java NIO 核心组件中的一个, 用于检查一个或多个 NIO Channel(通道)的状态是否处于可读、可写
可以实现单线程管理多个 channels, 相比使用多个线程, 避免了线程上下文切换带来的开销

多路复用

单线程可以配合 Selector 完成对多个 Channel 可读写事件的监控,这称之为多路复用
多路复用仅针对网络 IO,普通文件 IO 无法利用多路复用
如果不用 Selector 的非阻塞模式,线程大部分时间都在做无用功,而 Selector 能够保证
有可连接事件时才去连接
有可读事件才去读取
有可写事件才去写入
限于网络传输能力,Channel 未必时时可写,一旦 Channel 可写,会触发 Selector 的可写事件

Selector 的使用方法

通过调用 Selector.open() 方法创建一个 Selector 对象,如下

  Selector selector = Selector.open();

将通道设置为非阻塞的

  socketChannel.configureBlocking(false);

注册 Channel 到 Selector,并且关注可读事件

  final SelectionKey key = socketChannel.register(selector, SelectionKey.OP_READ);

注册进Selector里的通道Channel必须是非阻塞的

  • SelectableChannel 抽象类有一个 configureBlocking() 方法, 用于使 Channel 处于阻塞模式或者非阻塞模式
  • 因为FileChannel没有继承SelectableChannel,所以FileChannel不能注册进选择器
  • SocketChannel, ServerSocketChannel, DatagramChannel都是继承SelectableChannel,所以都可以设置为非阻塞的

register方法

Channel中的register方法是将通道注册到selector中,里面有三个参数register(Selector sel, int ops,Object att)
1、参数sel是指的将channel注册进的选择器
2、第二个参数ops是指的感兴趣的事件
3、第三个参数att是附件,可以绑定伴随此通道的缓冲区

Selector维护三种选择键:

  • keys:保存了所有已注册且没有cancel的选择键,Set类型.可以通过selector.keys()获取
  • selectedKeys:已选择键集,即前一次操作期间,已经准备就绪的通道所对应的选择键.此集合为keys的子集.通过selector.selectedKeys()获取.
  • canceledKeys:已取消键.已经被取消但尚未取消注册(deregister)的选择键.此集合不可被访问.为keys()的子集.
    对于新的Selector实例,上述三个集合均为空.通过channel.register将导致keys()集合被添加.
    如果某个selectionKey.cancel()被调用,那么此key将会被添加到canceldKey集合中,在下一次selector选择期间,如果canceldKeys不为空,将会导致触发此key的deregister操作(释放资源,并从keys中移除).
    无论通过channel.close()还是通过selectionKey.cancel(),都会导致key被加入到cannceldKey中.

SelectionKeys

SelectionKeys指的是选择器中触发的所有事件,SelectKey键表示了一个特定的 Channel 对象和一个特定的 Selector 对象之间的注册关系

   // 获取该selectionKey对应的通道channel
   key.channel();
   // 获取该selectionKey对应的选择器selector
   key.selector();
   // 获取selector关注该通道的事件类型
   key.interestOps();

判断是否包含某种类型的事件

   // 获取改key关注的所有事件的值之“和”
   int opsSet = selectionKey.interestOps();
   // 因为ops类型的值都是2的n次,所以我们可以通过位与运算来获取事件的类型
   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;
   // 在二进制位与运算中,在同一位只有两方都为1的情况下才为1
   boolean isAccept = opsSet & SelectionKey.OP_ACCEPT
   boolean isConnect = opsSet & SelectionKey.OP_CONNECT;
   boolean isRead = opsSet & SelectionKey.OP_READ;
   boolean isWrite = opsSet & SelectionKey.OP_WRITE;

从 Selector 中获取就绪事件并得到对应的通道,然后进行处理

Selector 维护注册过的 Channel 集合, 并且这种注册关系被封装在 SelectionKey 中
通过 Selector 的 select() 方法可以选择已经准备就绪的 Channel
int select():阻塞到至少有一个通道在你注册的事件上就绪了
int select(long timeout):和 select() 一样,但最长阻塞时间为timeout毫秒
int selectNow():非阻塞,只要有通道就绪就立刻返回

select() 方法返回的 int 值表示有多少通道已经就绪,是自上次调用 select() 方法后有多少通道变成就绪状态

// 代码实例Demo

    public static void main(String[] args) {
        // 获得服务器通道
        try(ServerSocketChannel server = ServerSocketChannel.open()) {
            server.bind(new InetSocketAddress(8080));
            // 创建选择器
            Selector selector = Selector.open();
            // 通道必须设置为非阻塞模式
            server.configureBlocking(false);
            // 将通道注册到选择器中,并设置感兴趣的事件
            server.register(selector, SelectionKey.OP_ACCEPT);
            // 为serverKey设置感兴趣的事件
            while (true) {
                // 若没有事件就绪,线程会被阻塞,反之不会被阻塞。从而避免了CPU空转
                // 返回值为就绪的事件个数
                int ready = selector.select();
                System.out.println("selector ready counts : " + ready);
                // 获取所有事件
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                // 使用迭代器遍历事件
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    // 判断key的类型
                    if(key.isAcceptable()) {
                        // 获得key对应的channel
                        ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                        System.out.println("before accepting...");
                        // 获取连接
                        SocketChannel socketChannel = channel.accept();
                        System.out.println("after accepting...");
                        // 设置为非阻塞模式,同时将连接的通道也注册到选择其中,同时设置附件
                        socketChannel.configureBlocking(false);
                        ByteBuffer buffer = ByteBuffer.allocate(16);
                        socketChannel.register(selector, SelectionKey.OP_READ, buffer);
                        // 处理完毕后移除
                        iterator.remove();
                    } else if (key.isReadable()) {
                        SocketChannel channel = (SocketChannel) key.channel();
                        System.out.println("before reading...");
                        // 通过key获得附件(buffer)
                        ByteBuffer buffer = (ByteBuffer) key.attachment();
                        // 当客户端关闭时会向通道发送一个写事件,此时通过read读取到的值是-1
                        int read = channel.read(buffer);
                        if(read == -1) {
                        	// 移除key,关闭通道
                            key.cancel();
                            channel.close();
                        } else {
                            // 通过分隔符来分隔buffer中的数据
                            split(buffer);
                            // split通过“\n”来进行数据分割,如果一次读取数据,没有读取到分隔符,证明缓冲区太小,那么就进行扩容
                            if (buffer.position() == buffer.limit()) {
                                ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity()*2);
                                // 将旧buffer中的内容放入新的buffer中
                                buffer.flip();
                                newBuffer.put(buffer);
                                // 将新buffer放到key中作为附件
                                key.attach(newBuffer);
                            }
                        }
                        System.out.println("after reading...");
                        // 处理完毕后移除
                        iterator.remove();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void split(ByteBuffer buffer) {
        buffer.flip();
        for(int i = 0; i < buffer.limit(); i++) {
            // 遍历寻找分隔符
            // get(i)不会移动position
            if (buffer.get(i) == '\n') {
                // 缓冲区长度
                int length = i+1-buffer.position();
                ByteBuffer target = ByteBuffer.allocate(length);
                // 将前面的内容写入target缓冲区
                for(int j = 0; j < length; j++) {
                    // 将buffer中的数据写入target中
                    target.put(buffer.get());
                }
                // 打印结果
                ByteBufferUtil.debugAll(target);
            }
        }
        // 切换为写模式,但是缓冲区可能未读完,这里需要使用compact
        buffer.compact();
    }

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值