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元素。

相关推荐
魔道不误砍柴功1 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2341 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨1 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
_.Switch1 小时前
高级Python自动化运维:容器安全与网络策略的深度解析
运维·网络·python·安全·自动化·devops
2401_850410831 小时前
文件系统和日志管理
linux·运维·服务器
qq_254674412 小时前
工作流初始错误 泛微提交流程提示_泛微协同办公平台E-cology8.0版本后台维护手册(11)–系统参数设置
网络
JokerSZ.2 小时前
【基于LSM的ELF文件安全模块设计】参考
运维·网络·安全
测开小菜鸟3 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
一只哒布刘3 小时前
NFS服务器
运维·服务器