Java NIO Selector简介

本文转载于本人个人博客

1.概述

在本文中,我们将探讨Java NIO的Selector组件的介绍部分。

选择器提供用于监视一个或多个NIO通道并识别何时其中的CHannel可用于数据传输的机制。

这样,单个线程可用于管理多个通道,从而管理多个网络连接。

2.为什么使用选择器?

使用选择器,我们可以使用一个线程而不是几个来管理多个通道。线程之间的上下文切换对于操作系统来说是昂贵的,此外,每个线程占用内存。

因此,我们使用的线程越少越好。尽管如此,我们仍需要记住现代操作系统和CPU在多任务处理方面越来越好,因此多线程的开销随着时间的推移而不断减少,适当的线程数有利于提高系统的并发能力。

我们将在这里讨论如何使用选择器使用单个线程处理多个通道。

另请注意,选择器不仅可以帮助你读取数据; 他们还可以监听传入的网络连接并通过慢速通道写入数据。

3.设置

要使用选择器,我们不需要任何特殊设置。我们需要的所有类都是核心的java.nio包,我们只需要导入我们需要的东西。

之后,我们可以使用选择器对象注册多个通道。当任何通道上发生I/O活动时,选择器会通知我们。这就是我们如何从单个线程中读取大量数据源。

我们向选择器注册的任何通道必须是SelectableChannel的子类。这些是一种特殊类型的通道,可以置于非阻塞模式。

4.创建选择器

可以通过调用Selector类的静态open方法来创建选择器,该方法将使用系统的默认选择器提供程序来创建新的选择器:

Selector selector = Selector.open();

5.注册可选择的频道

为了使选择器监视任何通道,我们必须使用选择器注册这些通道。我们通过调用selectable Channel的 register方法来完成此操作。

但是在使用选择器注册通道之前,它必须处于非阻塞模式:

channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

这意味着我们不能将FileChannel与选择器一起使用,因为它们不能像我们使用套接字通道那样切换到非阻塞模式。更重要的是FileChannel不是SelectableChannel的子类,没有register方法。

第一个参数是我们之前创建的Selector对象,第二个参数定义了一个兴趣集,这意味着我们通过选择器在所监视的通道中监听的感兴趣的事件是什么。

我们可以监听四种不同的事件,每种事件都由SelectionKey类中的常量表示:

  • Connect-当客户端尝试连接到服务器时。由SelectionKey.OP_CONNECT表示
  • Accept-当服务器接受来自客户端的连接时。由SelectionKey.OP_ACCEPT表示
  • Read-服务器准备好从通道读取时。由SelectionKey.OP_READ代表
  • Write-服务器准备好写入通道时。由SelectionKey.OP_WRITE代表

返回的对象SelectionKey表示通道对选择器的注册。我们将在下一节中进一步探讨。

6. SelectionKey对象

正如我们在上一节中看到的,当我们使用选择器注册一个通道时,我们得到一个SelectionKey对象。该对象保存表示通道注册的数据。

它包含一些重要的属性,我们必须很好地理解它们才能在通道上使用选择器。我们将在以下小节中查看这些属性。

6.1 兴趣集

兴趣集定义了我们希望选择器在此通道上注意的事件集。它是一个整数值; 我们可以通过以下方式获取此信息。

首先,我们有SelectionKey的interestOps方法返回的兴趣集。然后我们在之前看过的SelectionKey中有事件常量。

当我们对这两个值进行AND时,我们得到一个布尔值,告诉我们是否正在监视该事件:

int interestSet = selectionKey.interestOps();

boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;

6.2 就绪集

就绪集定义了通道准备好的事件集。它也是一个整数值; 我们可以通过以下方式获取此信息。

我们已经获得了SelectionKey的readyOps方法返回的就绪集。当我们将这个值与事件常量进行AND时,就像我们在兴趣集下所做的那样,我们得到一个布尔值来表示通道是否为特定值做好了准备。

另一种替代方法和更短的方法是使用SelectionKey的便捷方法来达到同样的目的:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWriteable();

6.3 获取key的通道

从SelectionKey对象访问正在监控的通道非常简单。我们只需调用channel方法:

Channel channel = key.channel();

6.4 选择器

就像获取通道一样,从SelectionKey对象获取Selector对象非常容易:

Selector selector = key.selector();

6.5 附加对象

我们可以将一个对象附加到SelectionKey。有时我们可能希望为通道提供自定义ID或附加我们可能想要跟踪的任何类型的Java对象。

附加对象是一种方便的方法。以下是如何从SelectionKey附加和获取对象:

key.attach(Object);

Object object = key.attachment();

或者,我们可以选择在频道注册期间附加对象。我们将它作为第三个参数添加到通道的寄存器方法中,如下所示:

SelectionKey key = channel.register(
  selector, SelectionKey.OP_ACCEPT, object);

7. 轮询就绪的事件

到目前为止,我们已经研究了如何创建选择器,向其注册通道以及检查SelectionKey对象的属性,该对象表示通道对选择器的注册。

这只是整个过程的一半,现在我们必须执行一个连续的过程来选择我们之前看过的就绪集。我们使用selector的select方法进行选择,如下所示:

int channels = selector.select();

该方法阻塞直到至少一个通道准备好进行操作。返回的整数表示其通道已准备好进行操作的键的数量。

接下来,我们通常检索出选定的键的集合并对其进行处理:

Set<SelectionKey> selectedKeys = selector.selectedKeys();

我们获得的集合是SelectionKey对象,每个Key代表一个已准备好进行操作的注册通道。

在此之后,我们通常迭代这个集合,对于每个Key,我们获得通道并执行我们兴趣集中出现的任何操作。

在频道通道的生命周期中,可以多次选择它,因为其键出现在不同事件的就绪集中。这就是为什么我们必须有一个连续的循环来捕获和处理它们发生时的事件。

8.完整示例

为了巩固我们在前面部分中获得的知识,我们将构建一个完整的客户端 - 服务器示例。

为了便于测试我们的代码,我们将构建一个echo服务器和一个echo客户端。在这种设置中,客户端连接到服务器并开始向其发送消息。服务器回送每个客户端发送的消息。

当服务器遇到特定消息(例如end)时,它会将其解释为通信结束并关闭与客户端的连接。

8.1 服务器

这是我们的EchoServer.java代码:

public class EchoServer {

    private static final String POISON_PILL = "POISON_PILL";

    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.bind(new InetSocketAddress("localhost", 5454));
        serverSocket.configureBlocking(false);
        serverSocket.register(selector, SelectionKey.OP_ACCEPT);
        ByteBuffer buffer = ByteBuffer.allocate(256);

        while (true) {
            selector.select();
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iter = selectedKeys.iterator();
            while (iter.hasNext()) {

                SelectionKey key = iter.next();

                if (key.isAcceptable()) {
                    register(selector, serverSocket);
                }

                if (key.isReadable()) {
                    answerWithEcho(buffer, key);
                }
                iter.remove();
            }
        }
    }

    private static void answerWithEcho(ByteBuffer buffer, SelectionKey key)
      throws IOException {

        SocketChannel client = (SocketChannel) key.channel();
        client.read(buffer);
        if (new String(buffer.array()).trim().equals(POISON_PILL)) {
            client.close();
            System.out.println("Not accepting client messages anymore");
        }

        buffer.flip();
        client.write(buffer);
        buffer.clear();
    }

    private static void register(Selector selector, ServerSocketChannel serverSocket)
      throws IOException {

        SocketChannel client = serverSocket.accept();
        client.configureBlocking(false);
        client.register(selector, SelectionKey.OP_READ);
    }


}

我们通过调用静态open方法创建一个Selector对象。然后,我们通过调用其静态open方法(特别是ServerSocketChannel实例)来创建通道。

这是因为ServerSocketChannel是可选择的,适用于面向流的侦听套接字。

然后我们将它绑定到我们选择的端口。请记住我们之前说过,在将可选择的通道注册到选择器之前,我们必须先将其设置为非阻塞模式。

我们在这个阶段不需要此频道的SelectionKey实例,因此我们不会保存它。

Java NIO使用面向缓冲区的模型,而不是面向流的模型。因此,套接字通信通常通过写入和读取缓冲区来进行。

因此,我们创建一个新的ByteBuffer,服务器将写入和读取。我们将它初始化为256个字节,它只是一个任意值,具体取决于我们计划来回传输的数据量。

最后,我们执行select。我们选择就绪通道,检索其SelectionKey,迭代它们并执行每个通道准备就绪的操作。

我们在无限循环中执行此操作,因为无论是否存在活动,服务器通常都需要继续运行。

ServerSocketChannel可以处理的唯一操作是ACCEPT操作。当我们接受来自客户端的连接时,我们获得了一个SocketChannel对象,我们可以在其上进行读写操作。我们将其设置为非阻塞模式并将其注册为选择器的READ操作。

在随后的一个选择期间,此新通道将变为可读状态。我们检索它并将其内容读入缓冲区。如果它是一个echo服务器,我们必须将这些内容写回客户端。

当我们想要写入我们一直在读取的缓冲区时,我们必须调用flip()方法。

我们最后通过调用flip方法将缓冲区设置为写入模式,然后简单地写入它。

8.2 客户端

这是我们的EchoClient.java代码:

public class EchoClient {
    private static SocketChannel client;
    private static ByteBuffer buffer;
    private static EchoClient instance;

    public static EchoClient start() {
        if (instance == null)
            instance = new EchoClient();

        return instance;
    }

    public static void stop() throws IOException {
        client.close();
        buffer = null;
    }

    private EchoClient() {
        try {
            client = SocketChannel.open(new InetSocketAddress("localhost", 5454));
            buffer = ByteBuffer.allocate(256);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public String sendMessage(String msg) {
        buffer = ByteBuffer.wrap(msg.getBytes());
        String response = null;
        try {
            client.write(buffer);
            buffer.clear();
            client.read(buffer);
            response = new String(buffer.array()).trim();
            System.out.println("response=" + response);
            buffer.clear();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return response;

    }
}

客户端比服务器简单。

我们使用单例模式在start static方法中实例化它。我们从这个方法中调用私有构造函数。

在私有构造函数中,我们在绑定服务器通道的同一端口上打开一个连接,并且仍然在同一主机上。

然后,我们创建一个缓冲区,我们可以编写并从中读取。

最后,我们有一个sendMessage方法,它读取包装我们传递给它的任何字符串到一个字节缓冲区,该字节缓冲区通过通道传输到服务器。

然后,我们从客户端通道读取以获取服务器发送的消息。我们将此作为消息的回声返回。

猜你喜欢

转载自blog.csdn.net/Tales_/article/details/82533171