NIO(三) Selector使用(NIO综合)

Selector(选择器)能够管理一到多个Channel(通道),监听通道是否为事件做好准备。

一,使用Selector的好处


只需少量线程来处理多个通道, 从而管理多个网络连接。

二,Selector示例


服务端

// 创建channel,服务端监听连接使用ServerSocketChannel
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8000));

// 与Selector一起使用时,Channel必须处于非阻塞模式下。因为FileChannel不能切换到非阻塞模式,所以不能与Selector一起使用。
ssc.configureBlocking(false);

// 注册channel到selector
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);

// 创建buffer
ByteBuffer readBuff = ByteBuffer.allocate(1024);
ByteBuffer writeBUff = ByteBuffer.allocate(2048);

while (true) {
    // 阻塞直到有事件就绪
    int readyNum = selector.select();
    if (readyNum == 0) {
        continue;
    }

    Set<SelectionKey> keys = selector.selectedKeys();
    Iterator it = keys.iterator();

    while (it.hasNext()) {
        SelectionKey key = (SelectionKey) it.next();

        if (key.isAcceptable()) {
            SocketChannel socketChannel = ssc.accept();
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ);
            System.out.println("创建连接");

        } else if (key.isReadable()) {
            SocketChannel socketChannel = (SocketChannel) key.channel();
            
            int readLen = 0;
            readBuff.clear();
            StringBuffer sb = new StringBuffer();
            while ((readLen = socketChannel.read(readBuff)) > 0) {
                // 注意这里是错误写法!因为最后一次从channel读,buffer里可能有老数据,占不满,参见Buffer原理
//              sb.append(new String(readBuff.array()));
//              readBuff.clear();

                readBuff.flip();
                byte[] temp = new byte[readLen];
                readBuff.get(temp, 0, readLen);
                sb.append(new String(temp));
                readBuff.clear();
             }

            // 如果客户端关闭就关闭客户端channel
             if (-1 == readLen) {
                 socketChannel.close();
             }

            // 注意这个是覆盖
            key.interestOps(SelectionKey.OP_WRITE);
            System.out.println("接受客户端消息:" + sb.toString());

        } else if (key.isWritable()) {
            writeBUff.clear();
            String s = "hello " + new String(readBuff.array()).trim();
            writeBUff.put(s.getBytes());
            writeBUff.flip();
            SocketChannel socketChannel = (SocketChannel) key.channel();
            
            // 非阻塞模式write返回了可能还没写完
            while(writeBUff.hasRemaining()) {
                socketChannel.write(writeBUff);
            }

            key.interestOps(SelectionKey.OP_READ);
        }

        // 删除key,注意这一步必须,不然会重复处理
        it.remove();
    }
}
  • register()方法的第二个参数表示监听Channel时对什么事件感兴趣,可以监听四种不同类型的事件:Connect、Accept、Read、Write
  • 如果对不止一种事件感兴趣,那么可以用"位或"操作符将常量连接起来,如:int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE
  • Selector不会自己从已选择键集中移除SelectionKey,必须在处理完通道时自己remove移除。下次该通道变成就绪时,Selector会再次将其放入已选择键集中。

客户端

// 客户端使用SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000));

ByteBuffer writeBuff = ByteBuffer.allocate(32);
ByteBuffer readBuff = ByteBuffer.allocate(32);

writeBuff.put("alex".getBytes());
writeBuff.flip();

while (true) {
    writeBuff.rewind();
    socketChannel.write(writeBuff);

    readBuff.clear();
    // 这里会阻塞等服务端消息
    socketChannel.read(readBuff);

    readBuff.flip();
    System.out.println("接受服务端消息:" + new String(readBuff.array()));

    Thread.sleep(1000);
}

三,SelectionKey介绍


当向Selector注册Channel时,register()方法会返回一个SelectionKey对象, 用于 绑定Channel和Selector。
这个对象包含了一些属性:

  • interest集合
  • ready集合
  • Channel
  • Selector
  • 附加的对象(可选)

**1,****SelectionKey.****interestOps():**获取Selector对Channel感兴趣的操作集合。

最初该集合是Channel被注册到Selector时传进来的值,该集合不会被Selector改变, 但是可通过interestOps()改变: key.interestOps(SelectionKey. OP_WRITE )。
我们可以通过以下方法来判断Selector是否对Channel的某种事件感兴趣:

int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;

2, SelectionKey. readyOps(): 获取相关通道已就绪的操作。

它是 感兴趣 集合的子集,表示interests集合中从上次调用select()以后已经就绪的那些操作。

int readSet = selectionKey.readOps();
selectionKey.isAcceptable();//等价于selectionKey.readyOps()&SelectionKey.OP_ACCEPT
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

注意, 通过readyOps()方法返回的就绪状态指示只是一个提示,底层的通道在任何时候都会不断改变,而其他线程也可能在通道上执行操作并影响到它的就绪状态。另外,我们不能直接修改ready集合。

3,SelectionKey.channel() 和SelectionKey.selector()

从SelectionKey访问Channel和Selector很简单。如下:

Channel channel  = selectionKey.channel();
Selector selector = selectionKey.selector();

**4,SelectionKey.cancel():**取消特定的注册关系。

该方法调用之后,该SelectionKey对象将会被"拷贝"至已取消键的集合中,该键此时已经失效,但是该注册关系并不会立刻终结。 在下一次select()时,已取消键的集合中的元素会被清除,相应的注册关系也真正终结。

5,SelectionKey.attach():附加对象

可以将一个对象附着到SelectionKey上,这样就能方便的识别某个给定的Channel。例如,可以附加与Channel一起使用的Buffer,或者一个Runnable处理就绪的事件。

// 绑定
selectionKey.attach(theObject);

// 获取
Object attachedObj = selectionKey.attachment();

// 取消
// 如果附加的对象不再使用,一定要人为清除,因为垃圾回收器不会回收该对象,若不清除的话会成内存泄漏。
selectionKey.attach(null).

还可以在用register()方法向Selector注册Channel的时候附加对象。如:

SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

或者:

SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); 
// 附加了一个Acceptor对象,这是用来处理连接请求的Runnable
selectionKey.attach(new Acceptor(selector, serverSocketChannel));

四,Selector的一些方法


一旦向Selector注册了一或多个Channel,就可以调用几个重载的select方法。这些方法返回感兴趣的事件(如连接、接受、读写)已经就绪的那些Channel。
使用:selector.select()。

// 阻塞到至少有一个通道在注册的事件上就绪了
int select() 

// 最长会阻塞timeout毫秒(参数)
int select(long timeout) 

// 执行非阻塞的选择。如果自从前一次选择后,没有通道变成可选择的,则此方法直接返回零
int selectNow() 
  • select()方法返回的int值表示有多少通道已经就绪。亦即,自上次调用select()方法后有多少通道变成就绪状态。
  • 如果对第一个就绪的channel没有做任何操作(也没把key从 selector .selectedKeys()中删除),现在又有一个channel就绪, 执行 selector .selectedKeys()就会返回2个key,但是 selector .select()只会返回1 ,即上次select()完和这次之间只有一个就绪! 即select()返回的数量是增量的。

1,selector.keys():已注册的SelectionKey集合

返回所有与selector关联的channel所生成的SelectionKey集合。
并不是所有注册过的键都仍然有效,这个集合也可能是空的。
这个集合不能直接修改。

2,selector**.selectedKeys():已就绪的SelectionKey集合**

每个SelectionKey都关联一个 已经准备好至少一种interest操作的Channel。每个SelectionKey都有一个内嵌的ready集合,指示了所关联的Channel已就绪的事件。
SelectionKey可以直接从这个集合中移除,但不能添加。

3,已删除的****SelectionKey集合

这个集合包含了执行过selectionKey.cancel()的key,但它们还没有被注销。这个集合是selector对象的私有成员,因而无法直接访问。

4,wakeUp()

某个线程调用select()方法后阻塞了,只要让 其它线程调用阻塞selector对象的wakeup()方法,阻塞在select()方法上的线程就会立马结束阻塞。
如果有其它线程调用了wakeup()方法,但当前没有线程阻塞在select()方法上,下个调用select()方法的线程会立即"醒来"。

5,close()

用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效。通道本身并不会关闭。

五,Selector.select的过程


1,检查已取消键集合(执行过selectionKey.cancel会添加进来),如果该集合不为空,则清空该集合里的键,同时该集合中每个键也将从已注册键集合和已选择键集合中移除。(一个SelectionKey被取消时,并不会立刻从集合中移除,而是将该键"拷贝"至已取消键集合中,这种就是"延迟取消"策略。)
2,检查已注册键集合(准确说是该集合中每个键的interests集合)。操作系统底层会执行多路复用系统调用(比如select、epoll)去检测就绪的Channel,并关联上SelectionKey。一旦发现某个Channel就绪了,则会首先判断该SelectionKey是否已经存在在已选择键集合当中,如果已经存在,则更新该SelectionKey的ready集合,如果不存在,则首先清空该SelectionKey的ready集合,然后重设ready集合,最后将该键存至已选择集合中。

当更新ready集合时,在上次select()中已经就绪的事件不会被删除,也就是 ready集合中的元素是累积的,比如在第一次的selector对某个Channel的read和write操作感兴趣,在第一次执行select()时,该通道的read操作就绪,此时该通道对应的键中的ready集合存有read元素,在第二次执行select()时,该通道的write操作也就绪了,此时该通道对应的ready集合中将同时有read和write元素。

相关推荐
Bulestar_xx1 小时前
安全见闻(3)
网络·windows·安全
炸鸡配泡面1 小时前
Qt 12.28 day3
java·开发语言
get_money_1 小时前
代码随想录Day37 动态规划:完全背包理论基础,518.零钱兑换II,本周小结动态规划,377. 组合总和 Ⅳ,70. 爬楼梯(进阶版)。
java·笔记·算法·动态规划
get_money_1 小时前
代码随想录38 322. 零钱兑换,279.完全平方数,本周小结动态规划,139.单词拆分,动态规划:关于多重背包,你该了解这些!背包问题总结篇。
java·开发语言·笔记·算法·动态规划
憶巷3 小时前
设计模式的分类及作用
java·设计模式
向宇it4 小时前
【从零开始入门unity游戏开发之——C#篇36】C#的out协变和in逆变如何解决泛型委托的类型转换问题
java·开发语言·unity·c#·游戏引擎
天空之外1364 小时前
Spring Boot Actuator、Spring Boot Actuator使用、Spring Boot Actuator 监控、Spring程序监控
java·spring boot·spring
baihb10244 小时前
Docker 默认安装位置迁移
java·docker
B1nnnn丶4 小时前
通用导出任何对象列表数据的excel工具类
java·spring boot·excel
陶然同学4 小时前
什么是大数据?2022大数据时代
java·大数据