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();
                }

            }

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

相关推荐
froginwe113 分钟前
XML 查看器:深入理解与高效使用
开发语言
梓沂4 分钟前
pom.xml中dependencyManagement的作用
xml·java·前端
F20226974868 分钟前
使用 Python 爬取某网站简历模板(bs4/lxml+协程)
开发语言·python
一行玩python8 分钟前
Xerces-C,一个成熟的 C++ XML 解析库!
xml·c语言·开发语言·c++
太空漫步1111 分钟前
逆序对个数
java·算法·排序算法
养佳工具人20 分钟前
Java各种排序
java·算法·排序算法
Fly_hao.belief32 分钟前
泛型的讲解(泛型类、泛型方法、泛型接口)
java
我命由我1234538 分钟前
15.Java 网络编程(网络相关概念、InetAddress、NetworkInterface、TCP 网络通信、UDP 网络通信、超时中断)
java·开发语言·网络·后端·tcp/ip·udp·java-ee
sunshine__sun39 分钟前
自动化测试报错:Exception managing chrome: error decoding response body
java·前端·chrome
yangpipi-1 小时前
数据结构(C语言版)-4.树与二叉树
c语言·开发语言·数据结构