Java NIO——Selector选择器

一、简介

  • 1.1、Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到Selector(选择器)

  • 1.2、Selector 能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。

  • 1.3、只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。

  • 1.4、避免了多线程之间的上下文切换导致的开销。

Selector一般称为选择器 ,也称多路复用器,多条channel复用selector。channe通过注册到selector ,使selector对channel进行监听,实现尽可能少的线程管理多个连接。减少了 线程的使用,降低了因为线程的切换引起的不必要额资源浪费和多余的开销。
也是网络传输非堵塞的核心组件。

二、特点

  • 2.1、Netty 的 IO 线程 NioEventLoop 聚合了 Selector(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。

  • 2.2、当线程从某客户端 Socket 通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。

  • 2.3、线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。

  • 2.4、由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程挂起。

  • 2.5、一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。

三、Selector的作用

选择器提供选择执行已经就绪的任务的能力。从底层来看,Selector提供了询问通道是否已经准备好执行每个I/O操作的能力。Selector 允许单线程处理多个Channel。仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道,这样会大量的减少线程之间上下文切换的开销。

四、Selector类相关方法

Selector 类是一个抽象类, 常用方法和说明如下:

public abstract class Selector implements Closeable {

    public static Selector open();//得到一个选择器对象
    
    public abstract int select(long timeout);//监控所有注册的通道,当其中有 IO 操作可以进行时,将
                                             //对应的 SelectionKey 加入到内部集合中并返回,参数用来设置超时时间
    public abstract Set selectedKeys();//从内部集合中得到所有的 SelectionKey
}

注意事项:

  • 1、NIO中的 ServerSocketChannel功能类似ServerSocket,SocketChannel功能类似Socket。

  • 2、selector select()方法详解
    select()方法,可以查询出已经就绪的通道操作,这些就绪的状态集合,保存在一个元素是SelectionKey对象的Set集合中。

    • selector.select()//阻塞
    • selector.select(1000);//阻塞1000毫秒,在1000毫秒后返回
    • selector.selectNow();//不阻塞,立马返还
    • selector.wakeup();//唤醒selector

select()方法返回的int值,表示有多少通道已经就绪,更准确的说,是自前一次select方法以来到这一次select方法之间的时间段上,有多少通道变成就绪状态。

NIO 非阻塞 网络编程原理分析图

NIO 非阻塞 网络编程相关的(Selector、SelectionKey、ServerScoketChannel和SocketChannel) 关系梳理图


  • 1、当客户端连接时,会通过ServerSocketChannel 得到 SocketChannel。

  • 2、Selector 进行监听 select 方法, 返回有事件发生的通道的个数。

  • 3、将socketChannel注册到Selector上,register(Selector sel,int ops),一个selector上可以注册多个SocketChannel。

  • 4、注册后返回一个 SelectionKey, 会和该Selector 关联(集合)。

  • 5、进一步得到各个 SelectionKey (有事件发生)。

  • 6、在通过 SelectionKey 反向获取 SocketChannel,方法 channel()。

  • 7、可以通过 得到的 channel,完成业务处理。

五、可选择通道(SelectableChannel)

并不是所有的Channel,都是可以被Selector 复用的。比方说,FileChannel就不能被选择器复用。

判断一个Channel 能被Selector 复用,有一个前提:判断他是否继承了一个抽象类SelectableChannel。如果继承了SelectableChannel,则可以被复用,否则不能。

SelectableChannel类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。所有socket通道,都继承了SelectableChannel类都是可选择的,包括从管道(Pipe)对象的中获得的通道。而FileChannel类,没有继承SelectableChannel,因此是不是可选通道。

通道和选择器注册之后,他们是绑定的关系吗?

不是一对一的关系。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。

通道和选择器之间的关系,使用注册的方式完成。SelectableChannel可以被注册到Selector对象上,在注册的时候,需要指定通道的哪些操作,是Selector感兴趣的。

Channel注册到Selector

使用Channel.register(Selector sel,int ops)方法,将一个通道注册到一个选择器时。第一个参数,指定通道要注册的选择器是谁。第二个参数指定选择器需要查询的通道操作。

可以供选择器查询的通道操作,从类型来分,包括以下四种:

  • 1、可读 : SelectionKey.OP_READ
  • 2、可写 : SelectionKey.OP_WRITE
  • 3、连接 : SelectionKey.OP_CONNECT
  • 4、接收 : SelectionKey.OP_ACCEPT

如果Selector对通道的多操作类型感兴趣,可以用“位或”操作符来实现:int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE ;

注意,操作一词,是一个是使用非常泛滥,也是一个容易混淆的词。特别提醒的是,选择器查询的不是通道的操作,而是通道的某个操作的一种就绪状态。

一旦通道具备完成某个操作的条件,表示该通道的某个操作已经就绪,就可以被Selector查询到,程序可以对通道进行对应的操作。比方说,某个SocketChannel通道可以连接到一个服务器,则处于“连接就绪”(OP_CONNECT)。再比方说,一个ServerSocketChannel服务器通道准备好接收新进入的连接,则处于“接收就绪”(OP_ACCEPT)状态。还比方说,一个有数据可读的通道,可以说是“读就绪”(OP_READ)。一个等待写数据的通道可以说是“写就绪”(OP_WRITE)。

六、选择键(SelectionKey)

Channel和Selector的关系确定好后,并且一旦通道处于某种就绪的状态,就可以被选择器查询到。这个工作,使用选择器Selector的select()方法完成。select方法的作用,对感兴趣的通道操作,进行就绪状态的查询。

Selector可以不断的查询Channel中发生的操作的就绪状态。并且挑选感兴趣的操作就绪状态。一旦通道有操作的就绪状态达成,并且是Selector感兴趣的操作,就会被Selector选中,放入选择键集合中。

一个选择键,首先是包含了注册在Selector的通道操作的类型,比方说SelectionKey.OP_READ。也包含了特定的通道与特定的选择器之间的注册关系。

开发应用程序是,选择键是编程的关键。NIO的编程,就是根据对应的选择键,进行不同的业务逻辑处理。

选择键的概念,有点儿像事件的概念。

一个选择键有点儿像监听器模式里边的一个事件,但是又不是。由于Selector不是事件触发的模式,而是主动去查询的模式,所以不叫事件Event,而是叫SelectionKey选择键。

七、Selector的使用流程

7.1、创建Selector

Selector对象是通过调用静态工厂方法open()来实例化的,如下:

// 1、获取Selector选择器
Selector selector = Selector.open();

Selector的类方法open()内部是向SPI发出请求,通过默认的SelectorProvider对象获取一个新的实例。

7.2、将Channel注册到Selector

要实现Selector管理Channel,需要将channel注册到相应的Selector上,如下:

// 2、获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

// 3.设置为非阻塞
serverSocketChannel.configureBlocking(false);

// 4、绑定连接
serverSocketChannel.bind(new InetSocketAddress(SystemConfig.SOCKET_SERVER_PORT));

// 5、将通道注册到选择器上,并制定监听事件为:“接收”事件
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);

上面通过调用通道的register()方法会将它注册到一个选择器上。

首先需要注意的是:

  • 与Selector一起使用时,Channel必须处于非阻塞模式下,否则将抛出异常IllegalBlockingModeException。这意味着,FileChannel不能与Selector一起使用,因为FileChannel不能切换到非阻塞模式,而套接字相关的所有的通道都可以。

另外,还需要注意的是:

  • 一个通道,并没有一定要支持所有的四种操作。比如服务器通道ServerSocketChannel支持Accept 接受操作,而SocketChannel客户端通道则不支持。可以通过通道上的validOps()方法,来获取特定通道下所有支持的操作集合。

7.3、轮询查询就绪操作

通过Selector的select()方法,可以查询出已经就绪的通道操作,这些就绪的状态集合,包存在一个元素是SelectionKey对象的Set集合中。

下面是Selector几个重载的查询select()方法:

  • 1、select():阻塞到至少有一个通道在你注册的事件上就绪了。
  • 2、select(long timeout):和select()一样,但最长阻塞事件为timeout毫秒。
  • 3、selectNow():非阻塞,只要有通道就绪就立刻返回。

select()方法返回的int值,表示有多少通道已经就绪,更准确的说,是自前一次select方法以来到这一次select方法之间的时间段上,有多少通道变成就绪状态。

一旦调用select()方法,并且返回值不为0时,通过调用Selector的selectedKeys()方法来访问已选择键集合,然后迭代集合的每一个选择键元素,根据就绪操作的类型,完成对应的操作:

Set selectedKeys = selector.selectedKeys();

Iterator keyIterator = selectedKeys.iterator();

while(keyIterator.hasNext()) {

    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {

        // a connection was accepted by a ServerSocketChannel.

    } else if (key.isConnectable()) {

        // a connection was established with a remote server.

    } else if (key.isReadable()) {

        // a channel is ready for reading

    } else if (key.isWritable()) {

        // a channel is ready for writing

    }

    keyIterator.remove();

}

处理完成后,直接将选择键,从这个集合中移除,防止下一次循环的时候,被重复的处理。键可以但不能添加。试图向已选择的键的集合中添加元素将抛出java.lang.UnsupportedOperationException。

八、一个NIO 编程的简单实例

8.1、服务端:

/**
 * @Description: 服务端接收客户端传来的数据
 */
public class NIOServer {

    public static void main(String[] args) throws IOException {
        //创建ServerSocketChannel -> ServerSocket
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //得到一个Selecor对象
        Selector selector = Selector.open();

        //绑定一个端口6666, 在服务器端监听
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));

        //设置为非阻塞
        serverSocketChannel.configureBlocking(false);

        //把 serverSocketChannel 注册到  selector 关心 事件为 OP_ACCEPT
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        // 用轮询的方式,查询获取“准备就绪”的注册过的操作
        while (true){
            //这里我们等待1秒,如果没有事件发生, 返回
            if(selector.select(1000) == 0) { //没有事件发生
                System.out.println("服务器等待了1秒,无连接");
                continue;
            }

            //如果返回的>0, 就获取到相关的 selectionKey集合
            //1.如果返回的>0, 表示已经获取到关注的事件
            //2. selector.selectedKeys() 返回关注事件的集合
            // 通过 selectionKeys 反向获取通道
            Set selectionKeys = selector.selectedKeys();

            //遍历 Set, 使用迭代器遍历
            Iterator iterator = selectionKeys.iterator();

            while(iterator.hasNext()){
                //获取到SelectionKey
                SelectionKey key = iterator.next();
                
                //处理key时,需要从selectionKeys集合中删除,否则下次处理就会有问题
                iterator.remove();
                
                //根据key 对应的通道发生的事件做相应处理
                if(key.isAcceptable()) { //如果是 OP_ACCEPT, 有新的客户端连接
                    //该该客户端生成一个 SocketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    System.out.println("客户端连接成功 生成了一个 socketChannel " + socketChannel.hashCode());
                    //将  SocketChannel 设置为非阻塞
                    socketChannel.configureBlocking(false);
                    //将socketChannel 注册到selector, 关注事件为 OP_READ, 同时给socketChannel
                    //关联一个Buffer
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));

                    System.out.println("客户端连接后 ,注册的selectionkey 数量=" + selector.keys().size()); //2,3,4..
                }
                if(key.isReadable()) {  //发生 OP_READ
                    try {
                        //通过key 反向获取到对应channel
                        SocketChannel channel = (SocketChannel)key.channel();

                        //获取到该channel关联的buffer
                        ByteBuffer buffer = (ByteBuffer)key.attachment();
                        //如果是正常断开,read方法返回值是-1
                        int read = channel.read(buffer);
                        if(read == -1){
                            key.cancel();
                        }else {
                            System.out.println("form 客户端 " + new String(buffer.array()));
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                        //因为客户端断开了,因此需要将key取消(从selector的keys集合中真正删除key)
                        key.cancel();
                    }
                }
            }
        }
    }
}

8.2、客户端:

public class NIOClient2 {

    public static void main(String[] args) throws IOException {
        //得到一个网络通道
        SocketChannel socketChannel = SocketChannel.open();

        //设置非阻塞
        socketChannel.configureBlocking(false);
        //提供服务器端的ip 和 端口
        InetSocketAddress inetSocketAddress = new InetSocketAddress("217.0.0.1", 6666);

        //连接服务器
        //socketChannel.connect(inetSocketAddress);
        //连接服务器
        if (!socketChannel.connect(inetSocketAddress)) {

            while (!socketChannel.finishConnect()) {
                System.out.println("因为连接需要时间,客户端不会阻塞,可以做其它工作..");
            }
        }

        //...如果连接成功,就发送数据
        String str = "hello, world";
        //Wraps a byte array into a buffer
        ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
        //发送数据,将 buffer 数据写入 channel
        socketChannel.write(buffer);
        //完毕时,清除缓冲区内容
        buffer.clear();

        //关闭相关流
        socketChannel.close();
    }
}

九、SelectionKey

9.1、SelectionKey,表示 Selector 和网络通道的注册关系, 共四种:

  • int OP_ACCEPT:有新的网络连接可以 accept,值为 16
  • int OP_CONNECT:代表连接已经建立,值为 8
  • int OP_READ:代表读操作,值为 1
  • int OP_WRITE:代表写操作,值为 4

源码中:

public static final int OP_READ = 1 << 0; 
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;

9.2、SelectionKey相关方法

public abstract class SelectionKey {

    public abstract Selector selector();//得到与之关联的 Selector 对象
   
    public abstract SelectableChannel channel();//得到与之关联的通道

    public final Object attachment();//得到与之关联的共享数据

    public abstract SelectionKey interestOps(int ops);//设置或改变监听事件

    public final boolean isAcceptable();//是否可以 accept

    public final boolean isReadable();//是否可以读

    public final boolean isWritable();//是否可以写

}

十、ServerSocketChannel

10.1、ServerSocketChannel 在服务器端监听新的客户端 Socket 连接

10.2、相关方法如下

public abstract class ServerSocketChannel extends AbstractSelectableChannel implements NetworkChannel{

    public static ServerSocketChannel open();得到一个 ServerSocketChannel 通道
    
    public final ServerSocketChannel bind(SocketAddress local);设置服务器端端口号
    
    public final SelectableChannel configureBlocking(boolean block);设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
    
    public SocketChannel accept();接受一个连接,返回代表这个连接的通道对象
    
    public final SelectionKey register(Selector sel, int ops);注册一个选择器并设置监听事件
    
}

十一、SocketChannel

  • 10.1、SocketChannel,网络 IO 通道,具体负责进行读写操作。NIO 把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。

  • 10.2、相关方法如下

public abstract class SocketChannel extends AbstractSelectableChannel 
    implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel{
    
    public static SocketChannel open();//得到一个 SocketChannel 通道
    
    public final SelectableChannel configureBlocking(boolean block);//设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
    
    public boolean connect(SocketAddress remote);//连接服务器
    
    public boolean finishConnect();//如果上面的方法连接失败,接下来就要通过该方法完成连接操作
    
    public int write(ByteBuffer src);//往通道里写数据
    
    public int read(ByteBuffer dst);//从通道里读数据
    
    public final SelectionKey register(Selector sel, int ops, Object att);//注册一个选择器并设置监听事件,最后一个参数可以设置共享数据
    
    public final void close();//关闭通道
    
}

NIO编程小结

NIO编程的难度比同步阻塞BIO大很多。

请注意以上的代码中并没有考虑“半包读”和“半包写”,如果加上这些,代码将会更加复杂。

  • 1、客户端发起的连接操作是异步的,可以通过在多路复用器注册OP_CONNECT等待后续结果,不需要像之前的客户端那样被同步阻塞。

  • 2、SocketChannel的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样I/O通信线程就可以处理其他的链路,不需要同步等待这个链路可用。

  • 3、线程模型的优化:由于JDK的Selector在Linux等主流操作系统上通过epoll实现,它没有连接句柄数的限制(只受限于操作系统的最大句柄数或者对单个进程的句柄限制),这意味着一个Selector线程可以同时处理成千上万个客户端连接,而且性能不会随着客户端的增加而线性下降。因此,它非常适合做高性能、高负载的网络服务器。

参考:
https://www.cnblogs.com/crazymakercircle/p/9826906.html

https://www.cnblogs.com/CllOVER/p/13441282.html

https://www.cnblogs.com/snailclimb/p/9086334.html

https://wiki.jikexueyuan.com/project/java-nio-zh/java-nio-selector.html

你可能感兴趣的:(Java NIO——Selector选择器)