一 前言
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这方面的介绍,多家对比,大家可以去看看呦 ^ - ^
这里我就直接写一个比较基础的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();
}
}
今天写这个写了不少时间,明日继续更新,大家是不是都要考六级了呢 = -- = 艾