NIO选择器

1 选择器基础

您需要将之前创建的一个或多个可选择的通道注册到选择器对象中。
        一个表示通道和选择器的键将会被返回。
        选择键会记住您关心的通道。
        它们也会追踪对应的通道是否已经就绪。
        当您调用一个选择器对象的 select()方法时,相关的键值会被更新,用来检查所有被注册到该选择器的通道。
        您可以获取一个键的集合,从而找到当时已经就绪的通道。
        通过遍历这些键,您可以选择出每个从上次您调用 select( )开始直到现在,已经就绪的通道。
    
        从最基础的层面来看,选择器提供了询问通道是否已经准备好执行每个 I/0 操作的能力。
        
        调用者可以轻松地决定多个通道中的哪一个准备好要运行。
            有两种方式可以选择:
                被激发的线程可以处于休眠状态,直到一个或者多个注册到选择器的通道就绪,
                或者它也可以周期性地轮询选择器,看看从上次检查之后,是否有通道处于就绪状态。

1.1 选择器、可选择通道和选择键类

选择器(Selector)
            
选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。当这么做的时候,可以选择将被激发的线程挂起,直到有就绪的的通道。
                
可选择通道(SelectableChannel)
                
 这个抽象类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。FileChannel 对象不是可选择的,因为它们没有继承 SelectableChannel(见图 4-2)。所有 socket 通道都是可选择的,包括从管道(Pipe)对象的中获得的通道。SelectableChannel 可以被注册到 Selector 对象上,同时可以指定对那个选择器而言,那种操作是感兴趣的。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。
                
选择键(SelectionKey)
            
选 择 键 封 装 了 特 定 的 通 道 与 特 定 的 选 择 器 的 注 册 关 系 。 选 择 键 对 象 被SelectableChannel.register( ) 返回并提供一个表示这种注册关系的标记。选择键包含了两个比特集(以整数的形式进行编码),指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。

SelectableChannel(可选择通道)
    public abstract SelectionKey register (Selector sel, int ops)
        将通道注册到一个选择器上,如果注册阻塞的通道。会抛异常。
        通道一旦被注册,就不能回到阻塞状态。试图这么做,也会抛异常的。
    public abstract SelectionKey register (Selector sel, int ops,Object att)
        返回一个封装了两个对象的关系的选择键对象。最后的att就是附加的附件
    public abstract boolean isRegistered( );
        可以调用 isRegistered( )方法来检查一个通道是否被注册到任何一个选择器上。
        这个方法没有提供关于通道被注册到哪个选择器上的信息,而只能知道它至少被注册到了一个选择器上。
        在一个键被取消之后,直到通道被注销为止,可能有时间上的延迟。这个方法只是一个提示,而不是确切的答案。
    public abstract SelectionKey keyFor (Selector sel);
        返回与该通道和指定的选择器相关的键
    public abstract int validOps( );    
        获取特定的通道所支持的操作集合
    public abstract void configureBlocking (boolean block)
    public abstract boolean isBlocking( );
    public abstract Object blockingLock( );    

    通道在被注册到一个选择器上之前,必须先设置为非阻塞模式(通过调用 configureBlocking(false))。
    
    一个给定的通道可以被注册到多于一个 的选 择器上 ,而 且不 需要 知道它 被注 册了 那个 Selector 对象上 。

1.2 建立选择器

Selector selector = Selector.open( );
channel1.register (selector, SelectionKey.OP_READ);
选择器才是提供管理功能的对象,而不是可选择通道对象。选择器对象对注册到它之上的通道执行就绪选择,并管理选择键。

channel2.register (selector, SelectionKey.OP_WRITE);
channel3.register (selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);

readyCount = selector.select (10000);    //等待十秒后查询已经准备好的通道

  • SelectionKey.OP_ACCEPT —— 接收连接继续事件,表示服务器监听到了客户连接,服务器可以接收这个连接了
  • SelectionKey.OP_CONNECT —— 连接就绪事件,表示客户与服务器的连接已经建立成功
  • SelectionKey.OP_READ —— 读就绪事件,表示通道中已经有了可读的数据,可以执行读操作了(通道目前有数据,可以进行读操作了)
  • SelectionKey.OP_WRITE —— 写就绪事件,表示已经可以向通道写数据了(通道目前可以用于写操作)

这里 注意,下面两种,SelectionKey.OP_READ ,SelectionKey.OP_WRITE ,

  • 1.当向通道中注册SelectionKey.OP_READ事件后,如果客户端有向缓存中write数据,下次轮询时,则会 isReadable()=true;
  • 2.当向通道中注册SelectionKey.OP_WRITE事件后,这时你会发现当前轮询线程中isWritable()一直为ture,如果不设置为其他事件

2 选择键

SelectionKey(选择键)
    public static final int OP_READ
    public static final int OP_WRITE
    public static final int OP_CONNECT
    public static final int OP_ACCEPT
    public abstract SelectableChannel channel( );//返回与该键相关的SelectableChannel 对象
    public abstract Selector selector( );//返回相关的 Selector 对象
    public abstract void cancel( );//终结键对象和通道的关系,不会立即取消,只是会放在选择器的已取消的集合里,注册不会立即被取消,但键会立即失效。当再次调用选择器的select()时,已取消的键的集合中的被取消的键就会被清理掉。并且相应的注销也将完成。通道会被注销,而新的SelectionKey将被返回。
    public abstract boolean isValid( );//检查选择键是否仍然有效。
    public abstract int interestOps( );//获取当前的 interest 集合
    public abstract void interestOps (int ops);
    public abstract int readyOps( );//获取相关的通道的已经就绪的操作
    public final boolean isReadable( )
    public final boolean isWritable( )
    public final boolean isConnectable( )
    public final boolean isAcceptable( )
    public final Object attach (Object ob)//将在键对象中保存所提供的对象的引用。SelectionKey 类除了保存它之外,不会将它用于任何其他用途。任何一个之前保存在键中的附件引用都会被替换
    public final Object attachment( )//调用 attachment( )方法来获取与键关联的附件句柄。如果没有附件,或者显式地通过 null 方法进行过设置,这个方法将返回 null。

SelectionKey key = channel.register (selector, SelectionKey.OP_READ, myObject);
等价于:
SelectionKey key = channel.register (selector, SelectionKey.OP_READ);
key.attach (myObject);

一个键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系 。
        
当通道关闭时,所有相关的键会自动取消

当选择器关闭时,所有被注册到该选择器的通道都将被注销,并且相关的键将立即被无效化(取消)。一旦键被无效化,调用它的与选择相关的方法就将抛出 CancelledKeyException。

一个 SelectionKey 对象包含两个以整数形式进行编码的比特掩码:一个用于指示那些通道/选择器组合体所关心的操作(instrest 集合),另一个表示通道准备好要执行的操作(ready 集合)。 interest 集合可以通过调用键对象的 interestOps( )方法来获取.

这个 interset 集合永远不会被选择器改变,但您可以通过调用 interestOps( )方法并传入一个新的比特掩码参数来改变它。

当相关的 Selector 上的 select( )操作正在进行时改变键的 interest 集合,不会影响那个正在进行的选择操作。所有更改将会在 select( )的下一个调用中体现出来。

ready 集合是 interest集合的子集,并且表示了 interest 集合中从上次调用 select( )以来已经就绪的那些操作。

有四个通道操作可以被用于测试就绪状态
    isReadable( ),isWritable( ),isConnectable( ), 和 isAcceptable( )
    
选择器中不能直接改变键的ready集合。

attach和attachment方法是允许您在键上放置一个“附件”,并在后面获取它。

如果选择键的存续时间很长,但您附加的对象不应该存在那么长时间,请记得在完成后清理附件。否则,您附加的对象将不能被垃圾回收,您将会面临内存泄漏问题。

总体上说,SelectionKey 对象是线程安全的,但知道修改 interest 集合的操作是通过 Selector 对象进行同步的是很重要的。这可能会导致 interestOps( )方法的调用会阻塞不确定长的一段时间。

3 选择器

Selector(选择器)
    public static Selector open( ) throws IOException
        实例化
    public abstract boolean isOpen( );
        测试一个选择器是否处于被打开状态被打开状态
    public abstract void close( ) throws IOException;
        是使用的时候调用这个,释放内存,并将相关的选择键设置为无效。
    public abstract SelectionProvider provider( );
        决定由哪个 SelectorProvider 对象来创建给定的 Selector 实例
    public abstract int select( ) throws IOException;
    public abstract int select (long timeout) throws IOException;
    public abstract int selectNow( ) throws IOException;
    public abstract void wakeup( );
    public abstract Set keys( );
    public abstract Set selectedKeys( );    
    
    选择器维护了一个需要监控的通道的集合。

3.1 选择过程

Selector
    public abstract Set keys( );
        返回已注册的键的集合
    public abstract Set selectedKeys( );
        返回通道已经就绪的键的集合
    public abstract int select( ) throws IOException;
        无线阻塞,直到有通道就绪了
    public abstract int select (long timeout) throws IOException;
        设置超时时间,让该线程不要一直阻塞
    public abstract int selectNow( ) throws IOException;
        不等待若没有就绪的通道直接返回0
    public abstract void wakeup( );        
    
已注册的键的集合(Registered key set)
    通过keys()方法返回已注册的键的集合,注册是并不一定都仍有效,这个集合是不可以直接修改的,试图这么做会报 java.lang.UnsupportedOperationException

已选择的键的集合(Selected key set)
    已注册的键的集合的子集。这个集合的每个成员的相关通道都是被选择器判断为已经选择好的。并且包含interest集合中的操作。通过selectedKeys()方法返回。
    已选择的键的集合和redy集合是不一样的,已选择的键的集合是选择器中的集合,这些集合中的键所关联的通道已经准备好的操作就存在ready集合中,且ready内嵌在键中。
    键可以直接从这个集合中移除,但是不能添加。
    
已取消的键的集合(Cancelled key set)
    已注册的键的集合的子集,这个子集包含了cancel()方法被调用过的键(这个键已经被无效化),但他们还没有被注销。
    这个集合是选择器对象的私有成员,不能直接访问。

在一个刚初始化的Selector对象中,这三个集合都是空的。

Selector 类的核心是选择过程

选择操作是当三种形式的 select( )中的任意一种被调用时,由选择器执行的。不管是哪一种形式的调用,下面步骤将被执行:
    
    1.已取消的键的集合将会被检查。如果它是非空的,每个已取消的键的集合中的键将从另外两个集合中移除,并且相关的通道将被注销。这个步骤结束后,已取消的键的集合将是空的。
    
    2.已注册的键的集合中的键的 interest 集合将被检查。在这个步骤中的检查执行过后,对interest 集合的改动不会影响剩余的检查过程。
    
        调用select方法时,如果没有通道已经准备好,线程可能会在这时阻塞,通常会有一个超时值。

        对于那些还没准备好的通道将不会执行任何的操作。
    
        对于那些操作系统指示至少已经准备好 interest 集合中的一种操作的通道,将执行以下两种操作中的一种:
    
        a.如果通道的键还没有处于已选择的键的集合中,那么键的 ready 集合将被清空,然后表示操作系统发现的当前通道已经准备好的操作的比特掩码将被设置。

        b.否则,也就是键在已选择的键的集合中。键的 ready 集合将被表示操作系统发现的当前已经准备好的操作的比特掩码更新。所有之前的已经不再是就绪状态的操作不会被清除。事实上,所有的比特位都不会被清理。由操作系统决定的 ready 集合是与之前的 ready 集合按位分离的,一旦键被放置于选择器的已选择的键的集合中,它的 ready 集合将是累积的。比特位只会被设置,不会被清理。
    
    3.步骤 2 可能会花费很长时间,特别是所激发的线程处于休眠状态时。与该选择器相关的键可能会同时被取消。当步骤 2 结束时,步骤 1 将重新执行,以完成任意一个在选择进行的过程中,键已经被取消的通道的注销。
    
    4.select 操作返回的值是 ready 集合在步骤 2 中被修改的键的数量,而不是已选择的键的集合中的通道的总数。

    使用内部的已取消的键的集合来延迟注销,是一种防止线程在取消键时阻塞,并防止与正在进行的选择操作冲突的优化。

select选择流程图

Selector 类的 select( )方法有以下三种不同的形式:

selectNow()方法执行就绪检查过程,但不阻塞。如果当前没有通道就绪,它将立即返回 0。

public abstract int select( ) throws IOException;
    无线阻塞,直到有通道就绪了
public abstract int select (long timeout) throws IOException;
    设置超时时间,让该线程不要一直阻塞

3.2 停止选择过程

Selector
    public abstract void wakeup( );        
    
    wakeup( ),提供了使线程从被阻塞的 select( )方法中优雅地退出的能力。

    有三种方式可以唤醒在 select( )方法中睡眠的线程:
        调用 wakeup( )
        调用wakeup()方法会让选择器上的第一个还没有返回的选择操作立即返回。如果当前没有进行中的选择,那么下一次调用的select()方法会立即返回。后续的选择操纵会正常进行。
        在选择操作期间多次调用wakeup()方法与调用它一次没有什么不同。
        有时候,我们不需要这种延迟唤醒,那么在调用wakeup()后,再调用selectNow()方法来绕过这个问题。
        调用 close( )
        调用一个close()方法,任何一个在选择操作中阻塞的select()方法都会被唤醒,与选择器相关的通道会被注销,键也会被取消。
        
        调用 interrupt( )
        睡眠中的线程的interrupt()被调用,它的返回状态将被设置。如果唤醒线程之后将视图在通道上执行I/O操作,通道将立即关闭。
    

请注意这些方法中的任意一个都不会关闭任何一个相关的通道。中断一个选择器与中断一个通道是不一样的(参见 3.3 节)。选择器不会改变任意一个相关的通道,它只会检查它们的状态。当一个在 select( )方法中睡眠的线程中断时,对于通道的状态而言,是不会产生歧义的。

3.3 管理选择键

选择是累积的。
一旦一个选择器讲一个键添加到已选择的键的集合中,这个键就不会被移除
一旦一个键处于已选择集合中,这个键的ready集合将只会被设置,而不会被清理。

合理地使用选择器的秘诀是理解选择器维护的选择键集合所扮演的角色。

一旦通道上一个操作就绪时,相关联的键的read集合就会被清空,然后把已经就绪的操作加进来。然后这个键就会被添加到已选择的集合中。

清理一个 SelectKey 的 ready 集合的方式是将这个键从已选择的键的集合中移除。因为只有存在已选择集合中的键才被认为是合法的,这些键会长久存在,不会被清理,直到被移除。

close()方法与select()方法一样。也有可能一直阻塞。
还在进行的选择过程中,所有对close()的调用都会被阻塞,直到选择过程结束,或者执行选择的线程进入睡眠。

NIO完整例子

public class NIOServer {
    /*标识数字*/
    private  int flag = 0;
    /*缓冲区大小*/
    private  int BLOCK = 4096;
    /*接受数据缓冲区*/
    private ByteBuffer sendbuffer = ByteBuffer.allocateDirect(BLOCK);
    /*发送数据缓冲区*/
    private  ByteBuffer receivebuffer = ByteBuffer.allocateDirect(BLOCK);
    private Selector selector;

    public NIOServer(int port) throws IOException {
        // 打开服务器套接字通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 服务器配置为非阻塞
        serverSocketChannel.configureBlocking(false);
        // 检索与此通道关联的服务器套接字
        ServerSocket serverSocket = serverSocketChannel.socket();
        // 进行服务的绑定
        serverSocket.bind(new InetSocketAddress(port));
        // 通过open()方法找到Selector
        selector = Selector.open();
        // 注册到selector,等待连接
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("Server Start----8888:");
    }


    // 监听
    private void listen() throws IOException {
        while (true) {
            // 选择一组键,并且相应的通道已经打开
            selector.select();
            // 返回此选择器的已选择键集。
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                iterator.remove();
                handleKey(selectionKey);
            }
        }
    }

    // 处理请求
    private void handleKey(SelectionKey selectionKey) throws IOException {
        // 接受请求
        ServerSocketChannel server = null;
        SocketChannel client = null;
        String receiveText;
        String sendText;
        int count=0;
        // 测试此键的通道是否已准备好接受新的套接字连接。
        if (selectionKey.isAcceptable()) {
            // 返回为之创建此键的通道。
            server = (ServerSocketChannel) selectionKey.channel();
            // 接受到此通道套接字的连接。
            // 此方法返回的套接字通道(如果有)将处于阻塞模式。
            client = server.accept();
            // 配置为非阻塞
            client.configureBlocking(false);
            // 注册到selector,等待连接
            client.register(selector, SelectionKey.OP_READ);
        } else if (selectionKey.isReadable()) {
            // 返回为之创建此键的通道。
            client = (SocketChannel) selectionKey.channel();
            //将缓冲区清空以备下次读取
            receivebuffer.clear();
            //读取服务器发送来的数据到缓冲区中
            count = client.read(receivebuffer);
            if (count > 0) {
                receiveText = new String( receivebuffer.array(),0,count);
                System.out.println("服务器端接受客户端数据--:"+receiveText);
                client.register(selector, SelectionKey.OP_WRITE);
            }
        } else if (selectionKey.isWritable()) {
            //将缓冲区清空以备下次写入
            sendbuffer.clear();
            // 返回为之创建此键的通道。
            client = (SocketChannel) selectionKey.channel();
            sendText="message from server--" + flag++;
            //向缓冲区中输入数据
            sendbuffer.put(sendText.getBytes());
            //将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
            sendbuffer.flip();
            //输出到通道
            client.write(sendbuffer);
            System.out.println("服务器端向客户端发送数据--:"+sendText);
            client.register(selector, SelectionKey.OP_READ);
        }
    }

    /**
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        // TODO Auto-generated method stub
        int port = 8888;
        NIOServer server = new NIOServer(port);
        server.listen();
    }
}

4 异步关闭能力

任何时候都可能关闭一个通道或者取消一个选择键。除非我们用同步,否则键和通道的状态的变化都是意料之外的改变。
        
一个特定的键的集合中的一个键的存在不能保证键仍然是有效,或者通道仍然是打开的

关闭通道的过程不应该是一个耗时的操作。

请不要自己维护键的集合!理解选择的过程!

如果你试图使用一个已经失效的键,大多数方法将抛出CancelledKeyException。

5 选择过程的扩展性

但只使用一个线程来服务所有可选择的通道是否是一个好主意呢?这要看情况。
        
对单 CPU 的系统而言这可能是一个好主意,因为在任何情况下都只有一个线程能够运行。

在一个有 n 个 CPU 的系统上,当一个单一的线程线性地轮流处理每一个线程时,可能有 n-1 个 cpu 处于空闲状态。

在大量通道上执行就绪选择并不会有很大的开销,大多数工作是由底层操作系统完成的。

在第一个场景中,
    如果您想要将更多的线程来为通道提供服务,一个更好的策略是对所有的可选择通道使用一个选择器,并将对就绪通道的服务委托给其他线程。您只用一个线程监控通道的就绪状态并使用一个协调好的工作线程池来处理共接收到的数据。根据部署的条件,线程池的大小是可以调整的(或者它自己进行动态的调整)。对可选择通道的管理仍然是简单的,而简单的就是好的。

第二个场景中,
    某些通道要求比其他通道更高的响应速度,可以通过使用两个选择器来解决。
    
    一个为命令连接服务,另一个为普通连接服务。
    
    但这种场景也可以使用与第一个场景十分相似的办法来解决。通道可以根据功能由不同的工作线程来处理。它们可能可以是日志线程池,命令/控制线程池,状态请求线程池,等等。

猜你喜欢

转载自blog.csdn.net/qq_32844875/article/details/82355012