前面讲过阻塞和非阻塞模式的运行方式,下面学习一下selector的运行方式。先上代码:
public class SelectorServer {
public static void main(String[] args) throws Exception {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8000));
// selector需要使用非阻塞模式
serverSocketChannel.configureBlocking(false);
// 1、创建selector
Selector selector = Selector.open();
// 2、将channel注册到selector上
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
System.out.println("等待事件....");
// 3、等待事件监听
selector.select();
// 4、对事件的处理
Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
while (selectionKeys.hasNext()) {
SelectionKey key = selectionKeys.next();
// 事件处理完成后需要将key关掉
selectionKeys.remove();
// 处理
if(key.isAcceptable()) {
// 客户端连接事件,连接事件是由ServerSocketChannel发生的
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel channel = server.accept();
channel.configureBlocking(false);
// 5、注册客户端的读事件,当客户端发送数据后,可以进行数据处理
ByteBuffer buffer = ByteBuffer.allocate(16);
channel.register(selector, SelectionKey.OP_READ, buffer);
System.out.println("客户端连接:" + channel);
} else if(key.isReadable()) {
// 读事件,消息是由客户端发送的
try {
SocketChannel channel = (SocketChannel) key.channel();
// 获取客户端对应的ByteBuffer
ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
// 6、读消息
int result = channel.read(byteBuffer);
if(result == -1) {
// 8、处理客户端主动关闭事件
System.out.println("客户端主动关闭...");
key.cancel();
continue;
}
// 9、处理消息边界问题,此处为客户端发送的消息长度大于服务器端设置的长度
if(byteBuffer.position() == byteBuffer.limit()) {
// 客户端消息过长,一次读不完
ByteBuffer newBuffer = ByteBuffer.allocate(byteBuffer.capacity() * 2);
byteBuffer.flip();
newBuffer.put(byteBuffer);
key.attach(newBuffer);
continue;
}
byteBuffer.flip();
System.out.println("收到客户端:" + channel + "的消息:" + Charset.defaultCharset().decode(byteBuffer));
byteBuffer.compact();
} catch (Exception e) {
// 7、处理客户端意外关闭导致服务端异常的问题,服务端关闭会发送一个读事件
e.printStackTrace();
// 事件必须进行处理
key.cancel();
}
}
}
}
}
}
概念:单线程配合seletor对channel可读写事件的监控,称为多路复用(仅针对网络IO)。
1、selector的使用步骤
首先我们得知道selector的使用步骤,步骤如下:
1.1 创建selector
Selector selector = Selector.open();
1.2 注册channel
创建完selector后,我们需要将channel注册到selector上,方便selector进行管理
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8000));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
这里首先需要注意的是,selector是运行在非阻塞模式下的,因此,我们需要将channel设置成非阻塞模式才可以。然后在注册时,可以设置需要关注的事件以及挂载的和当前channel相关的附件。
1.3 等待事件监听
selector.select();
等待事件监听是一个阻塞方法,只有channel上发生事件后,才可以让线程继续往下执行。
1.4 事件的处理
selector监听到一个事件后,必须对事件进行处理。
2、selector事件种类
当我们在注册channel时,需要关注事件的种类。selector事件种类有以下几种:
- accept:会在有客户端连接请求时触发
- connect:是客户端的事件,在连接建立时触发
- read:可读事件,客户端发送消息到服务端时触发
- write:可写事件,服务端向客户端发送消息时,系统会检查内核写缓冲区是否可写(当发送的内容过多时,会导致写缓冲区已经满了,无法继续往内写),如果可写,则会触发写事件
3、说明事项
3.1 事件为什么必须处理
selector的select()方法会阻塞线程的执行,直到等到channel发生了事件后,才会唤醒线程继续往下执行;如果没有对事件进行处理,那么select()方法在进行判断时,就会发现仍然有事件需要去处理,所以就不会发生阻塞。因此,一旦有事件发生,我们那么处理掉,那么就将事件取消掉。
3.2 用完key后,为什么得将key移除掉
当我们调用register()方法注册channel时,会在selector上添加对应的channel,此时如果有事件发生,那么selector会在selectedKeys上添加对应的事件信息(channel+事件),当事件处理完后,那么就会将selectedKeys上对应channel的事件移除掉,但是此时对应的cahnnel还存在,也就是说selectedKeys上仍然保留有该信息;如果不将key移除掉,那么下次在处理事件的时候,就会因为存在以前的信息,而导致数据不准确。
4、select何时不阻塞
- 事件发生时:客户端发起连接请求,触发accept事件;客户端发送数据、正常关闭、异常关闭时,会触发read事件;channel可写,会触发write事件;在linux下nio bug发生时;
- 调用selector.wakeup()
- 调用selector.close()
- selector所在线程interrupt
Comments NOTHING