在本文中,我们将探讨Java NIO的Selector组件。 Selector是一个定义在java.nio.channels包中的抽象类。
选择器(selector)提供用于监视一个或多个NIO信道(channel)并识别这些通道每个通道什么时候可用于数据传输的机制,也就是什么时候变为available。
这样,单个线程就可以管理多个信道(channel),这样也算是多个网络connection了。
使用选择器(selector),我们可以使用一个线程而不是多个线程,使用一个线程来管理多个通道(channel)。 在线程之间切换对于操作系统是昂贵的,而且,每个线程还要花费内存。
因此,我们使用的线程越少越好。 当然了,现代操作系统和CPU在多任务处理中已经有了很不错的性能,所以多线程的开销随着时间的推移也在不断减少。
一会我们将会介绍如何在单个线程中使用选择器(selector)来处理多个通道(channel)。
而且值得的注意的是,选择器(selector)不仅仅帮助你读取数据; 它们还可以侦听进来的网络连接(connection)并通过慢通道(slow channel)进行写数据。
要使用选择器,我们不需要任何特殊的设置。 我们需要的所有类都在java.nio包中,我们只需要导入我们需要的。
之后,我们就可以使用选择器对象注册多个通道。 当I/O活动发生在任何通道上时,选择器就会通知我们。 这就是从单个线程上读取大量数据的方式。
我们在选择器上注册的任何通道必须是SelectableChannel的子类。 因为只有SelectableChannel的子类才能被设置为非阻塞模式。
创建一个Selector很简单,调用Selector类的静态open方法就可以创建一个选择器(Selector),该方法将使用系统默认的选择器(selector)的provider来创建一个新的选择器,像下面这样:
5. 注册已选通道
为了使选择器监视任何通道,我们必须让这些通道注册在选择器上。 我们通过调用已选通道register的方法来实现。
但在通道注册到选择器之前,它必须处于非阻塞模式:
也就是说,我们不能让FileChannel去注册到选择器上,因为它们不能切换到非阻塞模式。
第一个参数是我们之前创建的Selector对象,第二个参数定义一个兴趣设置,指的是我们想要监听通道中所感兴趣的事件。
我们可以监听四个不同的事件,每个都由SelectionKey类中的常量表示:
返回的对象SelectionKey表示通道注册到选择器后的一个综合结果。 我们将在下一节中进一步讨论。
正如我们在上一节中看到的,当我们把一个通道注册到选择器时,我们得到一个SelectionKey对象。 此对象保存了通道注册的数据。
它包含一些重要的属性,我们必须理解,以便能够使用通道上的选择器。 我们将在以下子节中查看这些属性。
兴趣集(interest set)定义了我们希望选择器在此频道上注意的事件集。 它是一个整数值; 我们可以通过以下方式获取此信息。
首先,我们有SelectionKey的interestOps方法返回的兴趣集(interest set)。 然后我们在之前我们看过的SelectionKey中有事件常量。
当我们AND这两个值时,我们得到一个布尔值,告诉事件是否被监视:
6.2. Ready Set
就绪集(ready set)定义了通道准备就绪的事件集。 它也是一个整数值; 我们可以通过以下方式获取此信息。
我们有SelectionKey的readyOps方法返回的ready集合。 当我们将这个值与事件常数进行AND操作时,我们得到一个布尔值,表示通道是否已针对特定值准备好。
另一种替代方法是使用SelectionKey的如下方法来达到同样的目的:
6.3. Channel
从SelectionKey对象访问正在监视的频道非常简单。 只需调用channel方法:
6.4. Selector
就像获取一个频道一样,很容易从SelectionKey对象中获取Selector对象:
6.5. Attaching Objects
我们可以将对象附加到SelectionKey。 有时我们可能想给一个频道一个自定义ID或附加任何种类的Java对象,来达到跟踪的目的。
下面是在SelectionKey上附加和获取对象的方法:
或者,我们可以选择在频道注册期间附加对象。 我们将它作为第三个参数添加到通道的register方法上,如下所示:
到目前为止,我们已经研究了如何创建一个选择器,注册通道到选择器,并查看SelectionKey对象的属性,我们也知道了SelectionKey表示一个通道注册到选择器的结果。
这只是过程的一半,现在我们必须执行一个连续的过程,选择我们之前看过的就绪集。 我们使用选择器的select方法做选择,如:
此方法阻塞,直到至少一个通道准备好进行操作。 返回的整数表示其通道已准备好进行操作的key的整数。
接下来,我们通常检索所选的key们进行处理:
我们获得的集合是SelectionKey对象,每个key表示一个准备好被操作的已注册通道。
之后,我们通常迭代这个集合,对于每个key,我们获得通道并执行出现在我们的兴趣集中的任何操作。
在频道的生命周期中,它可以被选择若干次,因为其key出现在针对不同事件的就绪集中。 这就是为什么我们必须有一个循环来捕获和处理通道上的那些发生的事件。
为了巩固我们在前面章节中获得的知识,我们将构建一个完整的客户端 - 服务器示例。
为了便于测试我们的代码,我们将构建一个server和一个client。 在这种设置中,客户端连接到server并开始向其发送消息。 server再返回每个客户端发送的消息。
当server遇到特定消息(例如end)时,它将其理解为通信的结束,并关闭与client的连接。
我们看看上面发生了什么:我们通过调用静态open方法创建一个Selector对象。然后我们通过调用它的静态open方法创建一个通道,而且还是一个ServerSocketChannel实例。
这是因为ServerSocketChannel是可以被注册到selector的,而且还特别适合用于面向流的侦听socket。
然后我们将它绑定到我们选择的端口。记住我们之前说过,在将通道注册到选择器之前,我们必须首先将其设置为非阻塞模式。接下来我们这样做,然后将通道注册到选择器。
在这个阶段我们不需要这个通道的SelectionKey实例,所以我们不会记录它。
Java NIO使用面向缓冲区模型(buffer-oriented model),而不是面向流模型(stream-oriented model)。因此,通常通过写入缓冲区和从缓冲区读取来进行socket通信。
因此,我们创建一个新的ByteBuffer,server将把数据写入到这里边,也会从这里读取。我们将它初始化为256个字节,它只是一个任意的值,这取决于我们计划传输多少数据。
最后,我们执行selection过程。我们select准备就绪的通道,检索它们的selection keys,然后遍历这些keys,并执行针对每个准备好的通道的操作。
我们在无限循环中这样做,因为server通常需要保持运行,无论是否有活动。
ServerSocketChannel可以处理的唯一操作是ACCEPT操作。当我们从客户端接受连接时,我们获得一个SocketChannel对象,我们可以在其上进行读取和写入。我们将其设置为非阻塞模式,并将其注册到选择器上,专门用于READ操作。
在接下来选择(selections)之一期间,此新通道将变为只读状态,而且是就绪状态。我们检索它并将读取其内容,然后写入到buffer中。作为一个echo server,我们必须将这些内容写回客户端。
如果我们想要把已读取到的的数据写入到一个buffer中,我们必须调用flip()方法。
我们最后通过调用flip方法将缓冲区设置为写模式,然后就可以轻松往里边写入了。
start()方法是用来在单元测试的时候启动server的。
客户端比服务器简单。
我们使用单例模式在静态start方法中实例化它。 我们从这个方法调用私有构造函数。
在私有构造函数中,我们打开一个connection,这个连接的端口和server端的端口一样,并且是同一个host。
然后我们创建一个buffer,然后我们就在这个buffer上进行写入和读取了。
最后,我们有一个sendMessage方法,它将我们传递给它的任何字符串包装到字节缓冲区中,该字节缓冲区通过通道传输到服务器。
然后,从客户端通道中读取server那边发过来的信息。
现在可以运行测试:
在本文中,我们已经介绍了Java NIO Selector组件的基本用法。
本文的完整源代码和所有代码段都可以在我的GitHub中找到。查看源码请点击“阅读原文”。⬇️
https://github.com/importsource/tuts/tree/master/tuts-nio-selector/src/main/java/com/importsource/tuts/nio/selector
本文分享自 ImportSource 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!