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元素。
  • 22
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
NIO selector 是 Java NIO (New IO) 中的一个重要概念,它主要用于监听多个通道 (Channel) 上的 IO 事件。下面是一个简单的 NIO selector 的例子: ``` import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; public class NioServer { public static void main(String[] args) throws IOException { // 创建 ServerSocketChannel ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); serverSocketChannel.bind(new InetSocketAddress(8080)); // 创建 selector Selector selector = Selector.open(); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { selector.select(); Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); if (key.isAcceptable()) { SocketChannel socketChannel = serverSocketChannel.accept(); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); int read = socketChannel.read(buffer); if (read > 0) { buffer.flip(); System.out.println(new String(buffer.array(), 0, read)); } } iterator.remove(); } } } } ``` 在这个例子中,我们创建了一个 `ServerSocketChannel` 并绑定到端口 8080,然后创建了一个 selector 并将 serverSocketChannel 注册到 selector 上,监听 `OP_ACCEPT` 事件。当 selector 检测到有新的连接请求时,它会创建一个 `SocketChannel` 并将其注册到 selector 上,监听 `OP_READ` 事件。当 selector 检测到有数据可读时,它会读取客户端发送的数据

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值