Bootstrap

Java NIO中的Selector使用方法

Java NIO中的Selector是一个可以检查一个或多个SelectableChannel实例是否准备好进行I/O操作的对象。使用Selector可以让你用一个线程来管理多个Channel,从而提高应用程序的效率。

使用Selector的基本步骤:

1. 打开一个Selector

首先,需要通过调用Selector.open()方法来创建一个Selector

Selector selector = Selector.open();

2. 将Channel注册到Selector上

为了使用Selector,必须将Channel注册到Selector上,并指定要监听的事件。可以通过调用SelectableChannel.register()方法来实现。

channel.configureBlocking(false); // 设置为非阻塞模式
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

在这里,channel是一个SelectableChannel的实例,比如SocketChannelServerSocketChannel。第二个参数是一个“兴趣集合”,表示你想要监听的事件类型(SelectionKey.OP_READSelectionKey.OP_WRITESelectionKey.OP_CONNECTSelectionKey.OP_ACCEPT)。

3. 轮询就绪的Channel

接下来,可以通过调用Selectorselect()方法来检查是否有就绪的Channel。

  • select(): 阻塞直到至少有一个Channel准备好进行操作。
  • select(long timeout): 阻塞直到至少有一个Channel准备好进行操作,或者超时。
  • selectNow(): 不阻塞,立即返回就绪Channel的数量。
int num = selector.select(); // 阻塞直到至少有一个Channel准备好

4. 处理就绪的Channel

一旦select()方法返回,表示至少有一个Channel准备好了。可以通过调用selectedKeys()方法来获取就绪Channel的SelectionKey集合。

Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    
    if(key.isAcceptable()) {
        // 处理OP_ACCEPT事件
    } else if (key.isConnectable()) {
        // 处理OP_CONNECT事件
    } else if (key.isReadable()) {
        // 处理OP_READ事件
    } else if (key.isWritable()) {
        // 处理OP_WRITE事件
    }
    
    keyIterator.remove(); // 处理完事件后,从selectedKeys集合中移除SelectionKey
}

5. 关闭Selector

当不再需要Selector时,应该调用其close()方法来释放它可能占用的资源。

selector.close();

注意事项

  • Channel必须设置为非阻塞模式,因为Selector只适用于非阻塞Channel。
  • 在处理完每个事件后,应该从selectedKeys集合中移除对应的SelectionKey,否则它会在下次select调用时再次出现在该集合中。
    通过上述步骤,可以有效地使用Selector来管理多个Channel,实现高效的I/O多路复用。

实例演示

创建服务端TestSelector类来接收连接请求:

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.*;
import java.util.Iterator;

@Slf4j
public class TestSelector {
    public static void main(String[] args) throws IOException {
        // 创建selector,管理多个channel
        Selector selector = Selector.open();

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 非阻塞模式
        serverSocketChannel.configureBlocking(false);

        // 将serverSocketChannel注册到selector上,将来事件发生后,可以通过SelectionKey来获取事件以及关联的channel
        SelectionKey sscSelectionKey = serverSocketChannel.register(selector, 0, null);
        // 监听accept事件
        sscSelectionKey.interestOps(SelectionKey.OP_ACCEPT);
        log.debug("register key:{}", sscSelectionKey);

        // 绑定到指定端口
        serverSocketChannel.bind(new InetSocketAddress(8080));

        while (true) {
            // select方法,阻塞,直到有事件发生
            selector.select();
            // 处理事件,拿到selectionKey集合,内部包含了所有发生的事件
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                log.debug("key:{}", key);
                // 获取发生事件的通道
                ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                // 接受连接请求,返回一个新的SocketChannel
                SocketChannel socketChannel = channel.accept();
                log.debug("socketChannel:{}", socketChannel);
            }
        }
    }
}

创建客户端Client类来发送链接请求:

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;

@Slf4j
public class Client {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost", 8080));
        System.out.println("waiting......");
    }
}

之后先启动服务端:
在这里插入图片描述
可以看到注册key已经成功返回。
再启动客户端:
在这里插入图片描述
查看服务端控制台:
在这里插入图片描述
可以看到accept事件已经被成功监听到。

;