Netty系列(五)selector

浮生半日闲 发布于 2023-03-12 24 次阅读


前面讲过阻塞和非阻塞模式的运行方式,下面学习一下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