非阻塞编程核心设计之Selector

1. 铺垫

在本文中,我们将探讨Java NIO的Selector组件。 Selector是一个定义在java.nio.channels包中的抽象类。

选择器(selector)提供用于监视一个或多个NIO信道(channel)并识别这些通道每个通道什么时候可用于数据传输的机制,也就是什么时候变为available。

这样,单个线程就可以管理多个信道(channel),这样也算是多个网络connection了。

2. 为什么使用 Selector?

使用选择器(selector),我们可以使用一个线程而不是多个线程,使用一个线程来管理多个通道(channel)。 在线程之间切换对于操作系统是昂贵的,而且,每个线程还要花费内存。

因此,我们使用的线程越少越好。 当然了,现代操作系统和CPU在多任务处理中已经有了很不错的性能,所以多线程的开销随着时间的推移也在不断减少。

一会我们将会介绍如何在单个线程中使用选择器(selector)来处理多个通道(channel)。

而且值得的注意的是,选择器(selector)不仅仅帮助你读取数据; 它们还可以侦听进来的网络连接(connection)并通过慢通道(slow channel)进行写数据。

3. Setup

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

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

我们在选择器上注册的任何通道必须是SelectableChannel的子类。 因为只有SelectableChannel的子类才能被设置为非阻塞模式。

4. 创建一个Selector

创建一个Selector很简单,调用Selector类的静态open方法就可以创建一个选择器(Selector),该方法将使用系统默认的选择器(selector)的provider来创建一个新的选择器,像下面这样:

5. 注册已选通道

为了使选择器监视任何通道,我们必须让这些通道注册在选择器上。 我们通过调用已选通道register的方法来实现。

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

也就是说,我们不能让FileChannel去注册到选择器上,因为它们不能切换到非阻塞模式。

第一个参数是我们之前创建的Selector对象,第二个参数定义一个兴趣设置,指的是我们想要监听通道中所感兴趣的事件。

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

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

返回的对象SelectionKey表示通道注册到选择器后的一个综合结果。 我们将在下一节中进一步讨论。

6. SelectionKey 对象

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

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

6.1. Interest Set

兴趣集(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方法上,如下所示:

7. Channel Key Selection

到目前为止,我们已经研究了如何创建一个选择器,注册通道到选择器,并查看SelectionKey对象的属性,我们也知道了SelectionKey表示一个通道注册到选择器的结果。

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

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

接下来,我们通常检索所选的key们进行处理:

我们获得的集合是SelectionKey对象,每个key表示一个准备好被操作的已注册通道。

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

在频道的生命周期中,它可以被选择若干次,因为其key出现在针对不同事件的就绪集中。 这就是为什么我们必须有一个循环来捕获和处理通道上的那些发生的事件。

8. 完整示例

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

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

当server遇到特定消息(例如end)时,它将其理解为通信的结束,并关闭与client的连接。

8.1. Server端代码

我们看看上面发生了什么:我们通过调用静态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的。

8.2. Client端代码

客户端比服务器简单。

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

在私有构造函数中,我们打开一个connection,这个连接的端口和server端的端口一样,并且是同一个host。

然后我们创建一个buffer,然后我们就在这个buffer上进行写入和读取了。

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

然后,从客户端通道中读取server那边发过来的信息。

8.3. 测试

现在可以运行测试:

9. 最后

在本文中,我们已经介绍了Java NIO Selector组件的基本用法。

本文的完整源代码和所有代码段都可以在我的GitHub中找到。查看源码请点击“阅读原文”。⬇️

https://github.com/importsource/tuts/tree/master/tuts-nio-selector/src/main/java/com/importsource/tuts/nio/selector

原文发布于微信公众号 - ImportSource(importsource)

原文发表时间:2017-03-20

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏醒者呆

掌握NIO,程序人生

就像新IO为java带来的革新那样,让我们也开启一段新的程序人生。 关键字:NIO,BIO,伪IO,AIO,多路复用选择器,通道,缓冲区,jdk研究,回...

46860
来自专栏平凡文摘

Java 非阻塞 IO 和异步 IO

38230
来自专栏编程札记

深入golang之---goroutine并发控制与通信

本文章通过goroutine同步与通信的一个典型场景-通知子goroutine退出运行,来深入讲解下golang的控制并发。

1.3K70
来自专栏安恒网络空间安全讲武堂

Sniper-OJ 练习平台多题WriteUp

题目 ### 图书管理系统(200) ### as fast as you can(50) ### md5-vs-injection(50) ### 2048...

81970
来自专栏腾讯移动品质中心TMQ的专栏

手机APP安装包缩减方案

安装包大小对于产品很重要 主要有如下几个原因: 1、手机APP安装包的大小会影响用户是否愿意花费流量来下载此APP; 2、包体越大下载过程越长,用户取消下载的可...

31860
来自专栏JackieZheng

探秘Tomcat——从一个简陋的Web服务器开始

前言:   无论是之前所在实习单位小到一个三五个人做的项目,还是如今一个在做的百人以上的产品,一直都能看到tomcat的身影。工作中经常遇到的操作就是启动和关闭...

21370
来自专栏CRPER折腾记

Angular 2 + 折腾记 :(2)初步认识angular2,不一样的开发模式

想来想去,概念这些东西不怎么想讲,更多的是想讲点实战性的内容。 所以有些东西跳过去了,小伙伴们请去看官方文档哈;跳跃性的前进,写的不好多包涵。。。

10220
来自专栏吴伟祥

Java 的 I/O 类库的基本架构 转

I/O 问题是任何编程语言都无法回避的问题,可以说 I/O 问题是整个人机交互的核心问题,因为 I/O 是机器获取和交换信息的主要渠道。在当今这个数据大爆炸时代...

7100
来自专栏前端那些事

Express4.x API (三):Response (译)

Express4.x API 译文 系列文章 技术库更迭较快,很难使译文和官方的API保持同步,更何况更多的大神看英文和中文一样的流畅,不会花时间去翻译--,所...

179100
来自专栏orientlu

FreeRTOS 软定时器实现

考虑平台硬件定时器个数限制的, FreeRTOS 通过一个 Daemon 任务(启动调度器时自动创建)管理软定时器, 满足用户定时需求. Daemon 任务会在...

20220

扫码关注云+社区

领取腾讯云代金券