NIO - selector简单介绍

一 前言

selector作为NIO当中三大组件之一,是处理NIO非阻塞模式下的核心组件,它允许一个单个线程管理多个通道。

NIO下的阻塞模式

因为对于阻塞模式下的NIO模式,存在很大的问题,即使在单线程下,对应的服务端也会一直进行等待客户端的连接,甚至在建立连接之后读写模式下也会阻塞,这就导致只能让当前访问结束之后才能进行下一个客户端的访问,无法并行访问。这里我们主要介绍

NIO下的非阻塞模式

对于NIO下的非阻塞模式,我们只需要对于channel通道关闭阻塞模式即可。

java 复制代码
        //1。注册连接 创建连接通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //1.1 设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);

Server层代码:

java 复制代码
public class Server {
    private static final Logger log = LoggerFactory.getLogger(Server.class);
    //创建集合,存储对应的客户端信息
    public static ArrayList<SocketChannel> socketChannels = new ArrayList<>();
    public static void main(String[] args) throws IOException {
        //0.设置byteBuffer缓冲区存储数据
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //1。注册连接 创建连接通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //1.1 设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        //2.设置监听端口
        serverSocketChannel.bind(new InetSocketAddress(8080));
        while (true) {
            log.debug("connecting");
            //3.创建与客户端之间的连接 每有一个客户端连接,都会从这里进行监听
            //3.1 非阻塞模式下,说客户端与服务端连接的建立在这里不会堵塞,会直接通行,但是这里如果没有对应的客户端访问
            //那么返还值就为NULL,根据这个我们可以加一些判断对于这些空值进行处理
            SocketChannel accept = serverSocketChannel.accept();
            if (accept != null) {
                //添加数据
                socketChannels.add(accept);
            }
            for (SocketChannel socketChannel : socketChannels) {
                //获取数据
                log.debug("准备读取数据了!");
                //在非阻塞模式下,这里的读取也不会再停止,对应的会继续运行,如果没有读取到数据将会返还为空
                int read = socketChannel.read(buffer.flip());
                //读取数据
                buffer.flip();
                if (read!=0){
                    log.debug(String.valueOf(buffer));
                    log.debug("数据读取完毕");
                }
                //清空数据变为读取 清空数据
                buffer.clear();
            }
        }
    }
}

Client层代码:

java 复制代码
//客户端
public class Client {
    public static void main(String[] args) throws IOException {
        //1.创建连接通道
        SocketChannel socketChannel = SocketChannel.open();
        //2.设置连接服务器的地址
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
        System.out.println("waiting for connection");
        socketChannel.write(Charset.defaultCharset().encode("Hello World!"));
    }
}

Tips: 在非阻塞模式下有两点需要注意, 在服务端与客户端之间创建连接的时候,如果当前没有服务端连接,返还的值变为NULL,如下操作

java 复制代码
            //那么返还值就为NULL,根据这个我们可以加一些判断对于这些空值进行处理
            SocketChannel accept = serverSocketChannel.accept();

另一方面,在进行读取操作的时候也是一样,如果对应的客户端没有发送消息,读取的数据就会为0,因此我们可以在此基础上添加一些判断条件,如下

java 复制代码
  int read = socketChannel.read(buffer.flip());
                //读取数据
                buffer.flip();
                if (read!=0){
                    log.debug(String.valueOf(buffer));
                    log.debug("数据读取完毕");
                }

缺点:但是NIO非阻塞模式解决了阻塞模式下各种操作执行之间的阻塞关系,不会因为当前没有客户端连接而阻塞,换为一直都在执行当中。但是同时的也带来了一定的问题:一直循环不断的连接(accept)与读(read),如果我们一直都没有客户端连接,那么就会造成CPU资源的浪费,即使没有数据读写,也会让CPU一直处于资源消耗中~

二 Slector模式

在我之前的博客当中有对于NIO一些基础知识的介绍,也有关Selector这方面的介绍,多家对比,大家可以去看看呦 ^ - ^

Netty - NIO基础学习-CSDN博客

这里我就直接写一个比较基础的Selector代码,其中先不包含读写,仅仅包含如何使用Selector进行与客户端之间建立连接,以及如何监听客户端,让客户端与服务端之间建立连接

Server层:

java 复制代码
public class Server {
    private static final Logger log = LoggerFactory.getLogger(Server.class);
    //创建集合,存储对应的客户端信息
    public static void main(String[] args) throws IOException {
        //1.创建Selector
        Selector selector = Selector.open();
        //2.创建服务端通道
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8080));
        //3.创建客户端与Selector之间的连接,将两者之间建立连接
        SelectionKey sscKey = ssc.register(selector, 0, null);
        //4.建立连接之后就需要绑定对应的channel的事件类型,事件类型包括四种:accept connect read write 是哪一种事件需要我们自己进行绑定
        //这里我们这个SelectionKey作为管理员只需要关注对应的客户端是否建立连接即可
        sscKey.interestOps(SelectionKey.OP_ACCEPT);
        while (true) {
            //5.使用select进行检查,如果没有事件发生就在这里阻塞,有事件发生才会继续进行 这里类似一个监听器,如果有连接这种事件发生才会执行之后的操作
            selector.select();
            //6.使用迭代器处理发生的事件 selectKeys当中会存储所有的KEY,这里如果我们想要对其进行更多的操作,例如删除。那么就必须使用到迭代器
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                //获取KEY
                SelectionKey key = iterator.next();
                log.debug("Selected key: {}", key);
                //6.使用KEY获取对应的SSC之后创建连接
                ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                channel.accept();
                log.debug("Accepted connection");
            }
        }
    }
}

Cilent层:

java 复制代码
//客户端
public class Client {
    public static void main(String[] args) throws IOException {
        //1.创建连接通道
        SocketChannel socketChannel = SocketChannel.open();
        //2.设置连接服务器的地址
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
        System.out.println("waiting for connection");
        socketChannel.write(Charset.defaultCharset().encode("Hello World!"));
    }
}

在使用Selector的时候需要注意几点:

1.建立与channel之间的关联

首先便是建立起Selector跟对应的Channel之间的关联

我们需要使用到ServerSocketChannel下的注册,用以建立关联

java 复制代码
//3.创建客户端与Selector之间的连接,将两者之间建立连接
        SelectionKey sscKey = ssc.register(selector, 0, null);

2.SelectionKey绑定对应的channel事件

这里先简单说一下channel的几个事件类型,主要有:

accept: 建立客户端与服务端之间的连接

connect: 客户端与服务端连接建立之后自动触发的

read: 读操作

write: 写操作

当前的selector只需要作为一个管理员,管理对应其自己的事件即可,所以我们需要设置与对应KEY的关联channel事件类型,如下图:

设定完成之后,如果触发了对应的事件,选择器就会监听到

3.触发事件select()

这里我们需要用到selector的核心方法 - select()方法。

select方法会处于阻塞状态,除非 :

1> 已注册通道好的已经开始发送I/O请求

2>线程中断

3>当前的选择器Slector已被关闭

select有一个返回值,代表的是当前选择器当中已经准备好I/O请求的通道个数

也就是说,客户端向服务端发送请求的时候,非阻塞状态才会被激活。

select成功激活之后,会将当前检测到的事件的SelectKey放进迭代器当中

但是迭代器当中的数据是不会自动删除的,这一点很重要

建立连接之后执行的代码逻辑如下:

java 复制代码
        while (true) {
            //5.使用select进行检查,如果没有事件发生就在这里阻塞,有事件发生才会继续进行 这里类似一个监听器,如果有连接这种事件发生才会执行之后的操作
            selector.select();
            //6.使用迭代器处理发生的事件 selectKeys当中会存储所有的KEY,这里如果我们想要对其进行更多的操作,例如删除。那么就必须使用到迭代器
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                //获取KEY
                SelectionKey key = iterator.next();
                log.debug("Selected key: {}", key);
                //6.使用KEY获取对应的SSC之后创建连接
                ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                channel.accept();
                log.debug("Accepted connection");
            }
        }

三 selector处理读写

我们需要谨记一个概念,一个Selector当中可以存储多个KEY,那么实际上读写操作也就是再创建一个KEY放入Selector当中,并设置对应的事件类型即可!

上文提到,只要有对应的事件触发,那么select就会将其放置到迭代器的循环当中,也就是说所有事件类型的KEY,都会被存放在其中,但是不同的事件类型实际上执行的代码是不一样的,所以我们需要在迭代循环的时候根据KEY的事件类型不同进行区分

综上,我们改良之后的Server代码如下:

java 复制代码
public class Server {
    private static final Logger log = LoggerFactory.getLogger(Server.class);
    //创建集合,存储对应的客户端信息
    public static void main(String[] args) throws IOException {
        //1.创建Selector
        Selector selector = Selector.open();
        //2.创建服务端通道
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8080));
        //3.创建客户端与Selector之间的连接,将两者之间建立连接
        SelectionKey sscKey = ssc.register(selector, 0, null);
        //4.建立连接之后就需要绑定对应的channel的事件类型,事件类型包括四种:accept connect read write 是哪一种事件需要我们自己进行绑定
        //这里我们这个SelectionKey作为管理员,只需要关注对应的客户端是否建立连接即可
        //我们设置当前的KEY用来专门管理客户端的连接 accept()
        sscKey.interestOps(SelectionKey.OP_ACCEPT);
        while (true) {
            //5.使用select进行检查,如果没有事件发生就在这里阻塞,有事件发生才会继续进行 这里类似一个监听器,如果有连接这种事件发生才会执行之后的操作
            selector.select();
            //6.使用迭代器处理发生的事件 selectKeys当中会存储所有的KEY,这里如果我们想要对其进行更多的操作,例如删除。那么就必须使用到迭代器
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                //获取KEY
                SelectionKey key = iterator.next();
                //判断对应的KEY的类型
                if (key.isAcceptable()) {
                    log.debug("Accept Selected key: {}", key);
                    //6.使用KEY获取对应的SSC之后创建连接
                    ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                    //创建连接,返还客户端连接通道
                    SocketChannel sc = serverSocketChannel.accept();
                    sc.configureBlocking(false);
                    SelectionKey ssKey = sc.register(selector, SelectionKey.OP_READ);
                    //绑定事件
                    ssKey.interestOps(SelectionKey.OP_READ);
                    log.debug("Accept connection");
                } else if (key.isReadable()) {
                    //如果对应的KEY是读取类型的
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    SocketChannel channel = (SocketChannel) key.channel();
                    channel.read(buffer);
                    buffer.flip();
                    System.out.println(buffer);
                    buffer.compact();
                }

            }
        }
    }
}

其实上面的代码没有变化,知识迭代的过程代码发生变化:

1>对读(Read)操作开创新的KEY

在连接之后,又注册了当前通道的KEY,设置其事件类型为READ,并且将其交给selector管理

java 复制代码
if (key.isAcceptable()) {
                    log.debug("Accept Selected key: {}", key);
                    //6.使用KEY获取对应的SSC之后创建连接
                    ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                    //创建连接,返还客户端连接通道
                    SocketChannel sc = serverSocketChannel.accept();
                    sc.configureBlocking(false);
                    SelectionKey ssKey = sc.register(selector, SelectionKey.OP_READ);
                    //绑定事件
                    ssKey.interestOps(SelectionKey.OP_READ);
                    log.debug("Accept connection");
                }

2>新增有关读取数据的操作

java 复制代码
else if (key.isReadable()) {
                    //如果对应的KEY是读取类型的
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    SocketChannel channel = (SocketChannel) key.channel();
                    channel.read(buffer);
                    buffer.flip();
                    System.out.println(buffer);
                    buffer.compact();
                }

但是以上代码还是存在弊端,运行之后,发现报错

分析一下案发现场:

我们上面提到,执行select之后,变为非阻塞状态,==》 之后会将对应的事件的KEY交给下边的迭代器集合。这里我们的客户端发送了连接申请,并且写入了数据。

1.那么我们的服务器检测到事件之后,将连接申请相关事件的KEY提交给selectKeys的集合当中(也就是迭代器集合)

2.类型匹配,匹配到了key.isAcceptable(),进入并且创建连接,又新增一个KEY,绑定读操作事件,当前if结束

3.循环回到accept(),检测到写操作,之后将写操作的KEY提交给selectKeys当中

4.类型匹配,我们发现,之前已经完成过的事件,也就是连接事件依旧存在于迭代循环当中!但是我们这个事件已经处理结束!因此,accept()之后的数据为NULL

真相大白,其实就是因为我们没有删除对应在selectKeys集合当中已执行的KEY所导致的。

这也就是为什么必须使用iterator.remove()的原因了。

在一开始迭代就删除当前的元素即可。

java 复制代码
while (iterator.hasNext()) {
                //在一开始执行就直接移除这个KEY
                iterator.remove();
                //获取KEY
                SelectionKey key = iterator.next();
                //判断对应的KEY的类型
                if (key.isAcceptable()) {
                    log.debug("Accept Selected key: {}", key);
                    //6.使用KEY获取对应的SSC之后创建连接
                    ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                    //创建连接,返还客户端连接通道
                    SocketChannel sc = serverSocketChannel.accept();
                    sc.configureBlocking(false);
                    SelectionKey ssKey = sc.register(selector, SelectionKey.OP_READ);
                    //绑定事件
                    ssKey.interestOps(SelectionKey.OP_READ);
                    log.debug("Accept connection");
                } else if (key.isReadable()) {
                    //如果对应的KEY是读取类型的
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    SocketChannel channel = (SocketChannel) key.channel();
                    channel.read(buffer);
                    buffer.flip();
                    System.out.println(buffer);
                    buffer.compact();
                }

            }

今天写这个写了不少时间,明日继续更新,大家是不是都要考六级了呢 = -- = 艾

相关推荐
lightqjx4 分钟前
【数据结构】顺序表(sequential list)
c语言·开发语言·数据结构·算法
巨人张13 分钟前
信息素养Python编程题
开发语言·python
东阳马生架构15 分钟前
订单初版—5.售后退货链路中的技术问题说明文档
java
小小寂寞的城21 分钟前
JAVA策略模式demo【设计模式系列】
java·设计模式·策略模式
阿猿收手吧!37 分钟前
【计算机网络】HTTP1.0 HTTP1.1 HTTP2.0 QUIC HTTP3 究极总结
开发语言·计算机网络
JAVA学习通38 分钟前
图书管理系统(完结版)
java·开发语言
abigalexy1 小时前
深入Java锁机制
java
paishishaba1 小时前
处理Web请求路径参数
java·开发语言·后端
七七七七071 小时前
C++类对象多态底层原理及扩展问题
开发语言·c++