一 前言
selector作为NIO当中三大组件之一,是处理NIO非阻塞模式下的核心组件,它允许一个单个线程管理多个通道。
NIO下的阻塞模式
因为对于阻塞模式下的NIO模式,存在很大的问题,即使在单线程下,对应的服务端也会一直进行等待客户端的连接,甚至在建立连接之后读写模式下也会阻塞,这就导致只能让当前访问结束之后才能进行下一个客户端的访问,无法并行访问。这里我们主要介绍
NIO下的非阻塞模式
对于NIO下的非阻塞模式,我们只需要对于channel通道关闭阻塞模式即可。
//1。注册连接 创建连接通道ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();//1.1 设置为非阻塞模式serverSocketChannel.configureBlocking(false);
Server层代码:
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层代码:
//客户端
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,如下操作
//那么返还值就为NULL,根据这个我们可以加一些判断对于这些空值进行处理SocketChannel accept = serverSocketChannel.accept();
另一方面,在进行读取操作的时候也是一样,如果对应的客户端没有发送消息,读取的数据就会为0,因此我们可以在此基础上添加一些判断条件,如下
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层:
public class Server {private static final Logger log = LoggerFactory.getLogger(Server.class);//创建集合,存储对应的客户端信息public static void main(String[] args) throws IOException {//1.创建SelectorSelector 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()) {//获取KEYSelectionKey key = iterator.next();log.debug("Selected key: {}", key);//6.使用KEY获取对应的SSC之后创建连接ServerSocketChannel channel = (ServerSocketChannel) key.channel();channel.accept();log.debug("Accepted connection");}}}
}
Cilent层:
//客户端
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下的注册,用以建立关联
//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放进迭代器当中
但是迭代器当中的数据是不会自动删除的,这一点很重要
建立连接之后执行的代码逻辑如下:
while (true) {//5.使用select进行检查,如果没有事件发生就在这里阻塞,有事件发生才会继续进行 这里类似一个监听器,如果有连接这种事件发生才会执行之后的操作selector.select();//6.使用迭代器处理发生的事件 selectKeys当中会存储所有的KEY,这里如果我们想要对其进行更多的操作,例如删除。那么就必须使用到迭代器Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {//获取KEYSelectionKey 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代码如下:
public class Server {private static final Logger log = LoggerFactory.getLogger(Server.class);//创建集合,存储对应的客户端信息public static void main(String[] args) throws IOException {//1.创建SelectorSelector 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()) {//获取KEYSelectionKey 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管理
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>新增有关读取数据的操作
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()的原因了。
在一开始迭代就删除当前的元素即可。
while (iterator.hasNext()) {//在一开始执行就直接移除这个KEYiterator.remove();//获取KEYSelectionKey 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();}}
今天写这个写了不少时间,明日继续更新,大家是不是都要考六级了呢 = -- = 艾