NIO笔记(四)之Selector

JDK版本

  1. jdk8

Selector

  1. Selector主要作用是作为SelectableChannel对象的多路复用器
  2. 基本原理:当客户端发送数据到服务端,内核从网卡复制数据成功后会调用一个回调函数(将Socket加入到事件列表),回调函数中维护一个事件列表,应用层获取此事件列表即可以得到所有感兴趣的事件
  3. Selector提供选择执行已经就绪的事件的能力,而这些就绪事件就是上图中的事件列表。Selector类似一个观察者,只要我们把需要探知的SocketChannel告诉Selector,当有事件发生时,Selector会传回一组SelectorKey,通过读取这些Key,就会获得刚刚注册过的SocketChannel,此时从这个SocketChannel读取数据是一定可以读取到的。
  4. SelectorSelectableChannel对象的多路复用器,多路复用的核心目的就是使用最少的线程去操作更多的Channel,当Selector管理大量Channel时,如何高效完成所有Channel上就绪事件的检查?不同平台的实现机制不同,对于window平台来说(WindowsSelectorImpl实现类),创建线程的个数根据Channel的数量决定,每注册1024(0-1023)个Channel就创建1个新的线程,不满足1024个时,使用主线程。
  5. 默认Channel接口是没有注册方法的,只有SelectableChannel抽象类才有,所以以下无特殊说明,所说的Channel都是指SelectableChannel

创建

  1. 创建Selector,方式一本质上是通过方式二创建的

    方式一:
    Selector selector = Selector.open( );
    
    方式二:
    SelectorProvider provider = SelectorProvider.provider();
    Selector selector = provider.openSelector();
    
  2. SelectorProvider是用于SelectorSelectableChannel的服务提供者,SelectableChannelSelector的open方法底层都是通过SelectorProvider来创建实例的

    @Test
    public void testSelectorProvider() throws Exception {
        SelectorProvider provider = SelectorProvider.provider();
        //两种方式等价 
        Selector selector = provider.openSelector();
        Selector selector1 = Selector.open();
    
        //两种方式等价
        ServerSocketChannel serverSocketChannel = provider.openServerSocketChannel();
        ServerSocketChannel serverSocketChanne2 = ServerSocketChannel.open();
    
        //两种方式等价
        SocketChannel socketChannel1 = provider.openSocketChannel();
        SocketChannel socketChannel2 = SocketChannel.open();
    
    }
    

注册Channel到Selector

  1. SelectableChannel类的SelectionKey register(Selector sel, int ops)方法注册当前的SelectableChannelSelector上(底层调用的其实是SelectorImpl的未公开的SelectionKey register(AbstractSelectableChannel ch,int ops,Object attachment)方法),并返回一个SelectionKey。在将SelectableChannel注册到Selector之前必须将SelectableChannel设置为非阻塞模式

    @Test
    public void testSelector() throws Exception {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //注册到Selector之前必须将Channel设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress("localhost", 8888));
        //创建多路复用选择器
        Selector selector = Selector.open();
        //false 新创建的Channel总是未注册的
        System.out.println(serverSocketChannel.isRegistered());
        SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //true
        System.out.println(serverSocketChannel.isRegistered());
        //mac系统输出:sun.nio.ch.KQueueSelectorImpl@45ff54e6
        System.out.println(selector);
        //sun.nio.ch.SelectionKeyImpl@6d5380c2
        System.out.println(selectionKey);
        //true
        System.out.println(selector.isOpen());
        selector.close();
        //false
        System.out.println(selector.isOpen());
        //true
        System.out.println(serverSocketChannel.isOpen());
        serverSocketChannel.close();
        //false
        System.out.println(serverSocketChannel.isOpen());
    
    }    
    
  2. 同一个SelectableChannel可以注册到不同的Selector上,并返回不同SelectionKey

    @Test
    public void SelectionKey() throws Exception {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress("localhost", 8888));
        Selector selector = Selector.open();
        SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //sun.nio.ch.SelectionKeyImpl@6d5380c2
        System.out.println(selectionKey);
        //keyFor()方法获取当前Channel注册到指定Selector上的SelectionKey对象
        SelectionKey selectionKey1 = serverSocketChannel.keyFor(selector);
        //sun.nio.ch.SelectionKeyImpl@6d5380c2
        System.out.println(selectionKey1);
    
        //同一个Channel注册到多个Selector上
        Selector selector2 = Selector.open();
        SelectionKey selectionKey2 = serverSocketChannel.register(selector2, SelectionKey.OP_ACCEPT);
        //sun.nio.ch.SelectionKeyImpl@45ff54e6
        System.out.println(selectionKey2);
        selector.close();
        selector2.close();
        serverSocketChannel.close();
    }
    
  3. 不同的SelectableChannel注册到相同的Selector,返回不同的SelectionKey

    @Test
    public void SelectionKey2() throws Exception {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress("localhost", 8888));
        Selector selector = Selector.open();
        SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //sun.nio.ch.SelectionKeyImpl@6d5380c2
        System.out.println(selectionKey);
    
        //不同的Channel注册到相同的Selector
        ServerSocketChannel serverSocketChannel2 = ServerSocketChannel.open();
        serverSocketChannel2.configureBlocking(false);
        serverSocketChannel2.bind(new InetSocketAddress("localhost", 9999));
        SelectionKey selectionKey2 = serverSocketChannel2.register(selector, SelectionKey.OP_ACCEPT);
        //sun.nio.ch.SelectionKeyImpl@45ff54e6
        System.out.println(selectionKey2);
    
        selector.close();
        serverSocketChannel.close();
    }
    
    
  4. 不同的SelectableChannel注册到不同的Selector,返回的SelectionKey不是同一个对象

    @Test
    public void SelectionKey3() throws Exception {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress("localhost", 8888));
        Selector selector = Selector.open();
        SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //sun.nio.ch.SelectionKeyImpl@6d5380c2
        System.out.println(selectionKey);
    
        //不同的Channel注册到不同的Selector
        Selector selector2 = Selector.open();
        ServerSocketChannel serverSocketChannel2 = ServerSocketChannel.open();
        serverSocketChannel2.configureBlocking(false);
        serverSocketChannel2.bind(new InetSocketAddress("localhost", 9999));
        SelectionKey selectionKey2 = serverSocketChannel2.register(selector2, SelectionKey.OP_ACCEPT);
        //sun.nio.ch.SelectionKeyImpl@45ff54e6
        System.out.println(selectionKey2);
    
        selector.close();
        selector2.close();
        serverSocketChannel.close();
        serverSocketChannel2.close();
    }
    
  5. 相同的SelectableChannel注册到相同的Selector,返回的SelectionKey是同一个对象

    @Test
    public void SelectionKey4() throws Exception {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress("localhost", 8888));
        Selector selector = Selector.open();
        SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //sun.nio.ch.SelectionKeyImpl@6d5380c2
        System.out.println(selectionKey);
    
        //相同的Channel重复注册到相同的Selector
        SelectionKey selectionKey2 = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //sun.nio.ch.SelectionKeyImpl@6d5380c2
        System.out.println(selectionKey2);
        selector.close();
        serverSocketChannel.close();
    }
    
  6. 总结

    • Channel在被注册到Selector之前,必须先设置非阻塞模式(socketChannel.configureBlocking(false))
    • 一个Channel可以被注册到多个Selector上,但对每个Selector而言,最好只注册一次。如果Selector上多次注册同一个Channel,返回的SelectionKey总是同一个实例,后注册的感兴趣的操作类型会覆盖之前的感兴趣的操作类型
    • 一个Channel在不同的Selector上注册,每次返回的SelectionKey都是不同的实例。

select()

  1. selector.select()阻塞直到发生以下

    • 已经注册的Channel准备就绪
    • 通过调用SelectorwakeUp()方法
    • 线程被中断(调用阻塞线程的interrupt方法,同时线程对中断进行了处理)
    • 调用Selectorclose()方法
  2. 验证阻塞

    @Test
    public void testSelectorBlock() throws Exception {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress("localhost", 8888));
    
        Selector selector = Selector.open();
        SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //启动后阻塞,使用telnet 127.0.0.1 8888 访问后开始向下执行
        int select = selector.select();
        //输出:1
        System.out.println(select);
        //16
        System.out.println(selectionKey.readyOps());
        serverSocketChannel.close();
    }
    
  3. select()返回值:更新已就绪(选择)的SelectionKey的集合的个数,该数目可能为0或者大于0。0表示已就绪(选择)的SelectionKey的集合的个数没有更改。从上一个select()调用之后进入就绪状态的通道的数量,之前的调用中就绪的,并且在本次调用中仍然就绪的通道不会被计入,而那些在前一次调用中已经就绪但已经不再处于就绪状态的通道也不会被计入。下面的iterator.remove();被注释,所以第二次输出会返回0

    /**
     * 测试selector为0
     *  1. telent 127.0.0.1 8888
     *  2. telnet 127.0.0.1 8888
     *  第一次输出:
     *      已就绪的SelectionKey的集合:[sun.nio.ch.SelectionKeyImpl@6d5380c2]
     *      1
     *      执行accept...
     *  第二次输出
     *      已就绪的SelectionKey的集合:[sun.nio.ch.SelectionKeyImpl@6d5380c2]
     *      0
     *      执行accept...
     * 两次已就绪的SelectionKey的集合没有变化,所以select返回0
     * @throws Exception
     */
    @Test
    public void testSelector0() throws Exception {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress("localhost", 8888));
        serverSocketChannel.configureBlocking(false);
        Selector selector = Selector.open();
        SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            int select = selector.select();
            //获取已经注册的SelectionKey的集合
            Set<SelectionKey> registered = selector.keys();
            //获取准备就绪(已选择)的SelectionKey的集合
            Set<SelectionKey> readyed = selector.selectedKeys();
            System.out.println("已就绪的SelectionKey的集合:" + readyed);
            System.out.println(select);
            Iterator<SelectionKey> iterator = readyed.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                if (key.isAcceptable()) {
                    System.out.println("执行accept...");
                    ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = serverChannel.accept();
                    socketChannel.configureBlocking(false);
                    ByteBuffer buffer = ByteBuffer.wrap(new byte[]{'a', '\n'});
                    socketChannel.write(buffer);
                    buffer.clear();
                    socketChannel.close();
                }
    
                /**
                 * 1. selector.selectedKeys()返回的已选择键集,包含了当前所有准备就绪的通道。
                 * 2. select()不阻塞时,一个新的Channel就绪了
                 * 3. 就绪一个Channel就要把此Channel的所有就绪的事件都处理完毕
                 * 4. selector不会从已选择的SelectionKey的集合中移除SelectionKey,所以需要手动移除
                 */
                //iterator.remove();
            }
        }
    }
    
  4. select()方法

    • selectNow():如果没有Channel准备就绪,立刻返回0
    • select(long timeout):如果没有Channel准备就绪,阻塞规定时间

wakeup()

  1. 调用wakeup()可以使阻塞在select()方法的线程返回,当然如果当前没有阻塞的select()方法,那么会让下一次select()直接返回,两个连续的select()操作之间多次调用wakeup()和只调用一次的效果是一样的。不过很多时候,我们并不能确定是否有线程阻塞在select()方法,也不想影响到下一次select()(只是因为某些事件,临时唤醒一下),那可以在wakeup()之后调用selectNow(),他会立即返回,也抵消了wakeup()的影响

  2. 为什么要唤醒?

    • 注册了新的Channel或者事件
    • Channel关闭,取消注册
    • 优先级更高的事件触发(如定时事件),希望得到及时处理
  3. 模拟唤醒

    @Test
    public void testWakeUp() throws Exception {
        Selector selector = Selector.open();
    
        new Thread(() -> {
            try {
                Thread.sleep(3000);
                //3s后唤醒主线程的阻塞的Selector
                selector.wakeup();
                //1
                System.out.println(selector.keys().size());
                //0
                System.out.println(selector.selectedKeys().size());
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }).start();
    
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress("localhost", 8888));
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //阻塞
        int select = selector.select();
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        Iterator<SelectionKey> iterator = selectionKeys.iterator();
        while (iterator.hasNext()) {
            SelectionKey selectionKey = iterator.next();
            if (selectionKey.isAcceptable()) {
                ServerSocketChannel ssk = (ServerSocketChannel) selectionKey.channel();
                SocketChannel socketChannel = ssk.accept();
                socketChannel.close();
            }
            iterator.remove();
        }
    
    
        selector.close();
        serverSocketChannel.close();
    
    }
    

SelectionKey

  1. SelectionKey封装了SelectableChannel与特定的Selector的注册关系
  2. Selector会维护三种SelectionKey集合
    • 已注册的SelectionKey的集合,通过keys()方法返回,可能包括已经取消的键
    • 已就绪(选择)的SelectionKey的集合,通过selectedKeys()方法返回,此集合是已注册的SelectionKey的集合的子集
    • 已取消的SelectionKey的集合(但是还没注销),通过cancelledKeys()方法返回,但是该方法是protected,即不能在其他包中直接调用。此集合是已注册的SelectionKey的集合的子集
  3. 当我们调用Selectorselect()方法时,SelectedKeys集合会被更新,通过遍历SelectedKeys,可以找到已经就绪的Channel,从而处理各种I/O事件
  4. select()调用时的逻辑
    • 检查已取消的SelectionKey的集合,如果非空,从已注册的SelectionKey的集合中移除所有存在于已取消的SelectionKey的集合中的SelectionKey对象,并将注销其Channel,同时清空

    • 向内核发起一个系统调用进行查询,以确定Selector上注册的每个Channel所关心的事件是否就绪。如果没有Channel已经准备好,线程可能会一直阻塞(select())、阻塞指定时间(select(timeout))或立即返回(selectNow()),这主要依赖于特定select方法的调用;

    • 系统调用返回,再次检查已取消的SelectionKey的集合

    • 系统调用返回后,对于那些没有就绪事件的Channel将不会有任何的操作,对于那些已经有就绪事件的Channel,将执行以下两种操作的一种:

      • 如果ChannelSelectionKey还未加入已就绪(选择)SelectionKey集合,将其添加到已就绪(选择)SelectionKey集合中,修改readyOps
      • 如果ChannelSelectionKey已经存在于已就绪(选择)SelectionKey集合,修改readyOps,所有之前记录在readyOps中已经不再是就绪状态的操作不会被清除
      AbstractPollSelectorImpl代码
      
      protected int updateSelectedKeys() {
          int numKeysUpdated = 0;
          // Skip zeroth entry; it is for interrupts only
          for (int i=channelOffset; i<totalChannels; i++) {
              int rOps = pollWrapper.getReventOps(i);
              if (rOps != 0) {
                  SelectionKeyImpl sk = channelArray[i];
                  pollWrapper.putReventOps(i, 0);
                  if (selectedKeys.contains(sk)) {
                      if (sk.channel.translateAndSetReadyOps(rOps, sk)) {
                          numKeysUpdated++;
                      }
                  } else {
                      sk.channel.translateAndSetReadyOps(rOps, sk);
                      //SelectionKeyImpl中的readyOps和interestOps
                      if ((sk.nioReadyOps() & sk.nioInterestOps()) != 0) {
                          selectedKeys.add(sk);
                          numKeysUpdated++;
                      }
                  }
              }
          }
          return numKeysUpdated;
      }
      
    • 返回的是从上一个select()之后,处于就绪状态的Channel数量,如果已经在就绪集合中,不会累计;比如我们注册read,并且在使用之后SelectionKey不移出,那么下次再有read事件过来,select方法可能会返回0就不会被记录,但是不代表没有感兴趣的事件

已注册的SelectionKey的集合

  1. 已注册的SelectionKey的集合,在sun.nio.ch.SelectorImpl源码可以看到

    public abstract class SelectorImpl
        extends AbstractSelector
    {
    
        // The set of keys with data ready for an operation
        protected Set<SelectionKey> selectedKeys;
    
        // The set of keys registered with this Selector
        protected HashSet<SelectionKey> keys;
    
        // Public views of the key sets
        private Set<SelectionKey> publicKeys;             // Immutable
        private Set<SelectionKey> publicSelectedKeys;     // Removal allowed, but not addition
    
        protected SelectorImpl(SelectorProvider sp) {
            super(sp);
            keys = new HashSet<SelectionKey>();
            selectedKeys = new HashSet<SelectionKey>();
            if (Util.atBugLevel("1.4")) {
                publicKeys = keys;
                publicSelectedKeys = selectedKeys;
            } else {
                publicKeys = Collections.unmodifiableSet(keys);
                publicSelectedKeys = Util.ungrowableSet(selectedKeys);
            }
        }
        //keys返回不可变的keys
        public Set<SelectionKey> keys() {
            if (!isOpen() && !Util.atBugLevel("1.4"))
                throw new ClosedSelectorException();
            return publicKeys;
        }
        ...省略...
    } 
    

已就绪的SelectionKey的集合

  1. 当调用select()方法的主要流程

    select ①lockAndDoSelect ②doSelect ③processDeregisterQueue,处理取消的SelectionKeys集合 ④updateSelectedKeys,更新已就绪的SelectionKeys集合 select
  2. 查看sun.nio.ch.SelectorImpl

    public abstract class SelectorImpl
        extends AbstractSelector
    {
    
        // The set of keys with data ready for an operation
        protected Set<SelectionKey> selectedKeys;
        ...省略...
    } 
    
  3. 在Mac系统上,抽象类SelectorImpl的实现类是sun.nio.ch.KQueueSelectorImpl,这里查看updateSelectedKeys方法

    private int updateSelectedKeys(int entries)
            throws IOException
        {
            int numKeysUpdated = 0;
            boolean interrupted = false;
            updateCount++;
    
            for (int i = 0; i < entries; i++) {
                int nextFD = kqueueWrapper.getDescriptor(i);
                if (nextFD == fd0) {
                    interrupted = true;
                } else {
                    MapEntry me = fdMap.get(Integer.valueOf(nextFD));
                    if (me != null) {
                        int rOps = kqueueWrapper.getReventOps(i);
                        SelectionKeyImpl ski = me.ski;
                        //selectedKeys就是就绪的selectedKeys的集合
                        if (selectedKeys.contains(ski)) {
                            if (me.updateCount != updateCount) {
                                if (ski.channel.translateAndSetReadyOps(rOps, ski)) {
                                    numKeysUpdated++;
                                    me.updateCount = updateCount;
                                }
                            } else {
                              ski.channel.translateAndUpdateReadyOps(rOps, ski);
                            }
                        } else {
                            ski.channel.translateAndSetReadyOps(rOps, ski);
                            if ((ski.nioReadyOps() & ski.nioInterestOps()) != 0) {
                                //添加就绪的selectedKey到就绪的selectedKeys集合中
                                selectedKeys.add(ski);
                                numKeysUpdated++;
                                me.updateCount = updateCount;
                            }
                        }
                    }
                }
            }
    
            if (interrupted) {
                // Clear the wakeup pipe
                synchronized (interruptLock) {
                    IOUtil.drain(fd0);
                    interruptTriggered = false;
                }
            }
            return numKeysUpdated;
    }
    

已取消的SelectionKey的集合

  1. sun.nio.ch.SelectorImpl类继承了父类AbstractSelector, 已取消的SelectionKey的集合定义在 sun.nio.ch.SelectorImpl类的父类AbstractSelector

    public abstract class AbstractSelector extends Selector
    {
    
        ...省略...
        //已取消的`SelectionKey`的集合
        private final Set<SelectionKey> cancelledKeys = new HashSet<SelectionKey>();
        
        void cancel(SelectionKey k) {
            synchronized (cancelledKeys) {
                cancelledKeys.add(k);
            }
        }
        //此方法在select()时调用,上面分析已就绪SelectionKeys集合时
        protected final Set<SelectionKey> cancelledKeys() {
            return cancelledKeys;
        }
        
        ...省略...
    }
    
  2. 无论是调用该SelectionKeycancel()方法,还是通过关闭某个SelectionKeySelectableChannel(关闭时会调用SelectionKeycancel()方法),该SelectionKey都会被加入到已取消的SelectionKey的集合中

  3. 延迟注销SelectionKey的操作,是为了在选择的过程中减少不必要的同步,不然注销和选择就要形成一定的互斥,因为注销潜在的代价比较高,可能需要释放各种资源。

interestOps

  1. interestOps()代表感兴趣事件的集合,有四个常量

    public static final int OP_READ = 1 << 0      ==1  ==0000 0001
    public static final int OP_WRITE = 1 << 2     ==4  ==0000 0100
    public static final int OP_CONNECT = 1 << 3   ==8  ==0000 1000 
    public static final int OP_ACCEPT = 1 << 4    ==16 ==0001 0000
    其实还有一个就是00表示取消所有监听键
    
  2. 运算

    sk.interestOps(sk.interestOps()& ~SelectionKey.OP_READ)
    等价于(~ 按位取反)
    sk.interestOps(sk.interestOps()& 1111 1110)
    "&~xx"  代表取消xx事件
    
    sk.interestOps()| SelectionKey.OP_READ 
    "|xx" 代表添加xx事件 
    
    int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE; 表示对多个事件感兴趣(read和write)
        
    

readyOps()

  1. readyOps()返回Channel已经准备就绪事件的集合,readyOps()集合是interestOps()集合的子集。并且表示了interest集合中从上次调用select()以来已经就绪的那些操作。例如:注册channel感兴趣的动作是OP_READ,OP_WRITE,sc.register(sel, SelectionKey.OP_WRITE|SelectionKey.OP_READ);调用interestOps()可以得到他对read和write感兴趣,但是如果该channel中没有数据,则只能是key.readyOps()==SelectionKey.OP_WRITE

  2. 检查等价方式

    (key.isWritable())
    等价于:
    if ((key.readyOps() & SelectionKey.OP_WRITE) != 0)
    

cancel()

  1. SelectionKey代表ChannelSelector的注册关系,当Channel在选择器里注册后,Channel在注销之前将一直保持注册状态。可以调用SelectionKey类的cancel()取消这种关系,但是这种注销关系并不是立即生效(此时仅仅添加到其Selector维护的已取消的SelectionKey集合中),而是在Selector下一次调用select()时取消这种关系
  2. 无论是调用Channelclose()方法,还是中断阻塞于该通道上I/O操作的线程来关闭该Channel,都会隐式地取消该该Channel的所有SelectionKey
  3. 如果Selector本身已关闭,则将注销该Channel,并且表示其注册的SelectionKey将立即无效
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值