0%
核心组件
Channel
- Nio Channel类似于Java Stream,但又有几点不同
- Channel是双向的,Stream是单向的
- Channel可以非阻塞的进行读写操作,而Stream需要等待io操作完成,也就是阻塞的。
- Channel的读操作或者写操作都是依赖Buffer的,Stream没有依赖
ServerSocketChannel
- Java NIO中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道, 就像标准IO中的ServerSocket一样。
- ServerSocketChannel类在 java.nio.channels包中。
SocketChannel
- Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。
- 可以通过以下2种方式创建SocketChannel:
- 打开一个SocketChannel并连接到互联网上的某台服务器。
- 一个新连接到达ServerSocketChannel时,会创建一个SocketChannel。
- 非阻塞模式与选择器搭配会工作的更好,通过将一或多个SocketChannel注册到Selector,可以询问选择器哪个通道已经准备好了读取,写入等。
DatagramChannel
- Java NIO中的DatagramChannel是一个能收发UDP包的通道。
- 因为UDP是无连接的网络协议,所以不能像其它通道那样读取和写入。
- 它发送和接收的是数据包。
FileChannel
- Java NIO中的FileChannel是一个连接到文件的通道。可以通过文件通道读写文件。
- FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下。
Buffer
- 一个 Buffer,本质上是内存中的一块,我们可以将数据写入这块内存,之后从这块内存获取数据。通过将这块内存封装成 NIO Buffer 对象,并提供了一组常用的方法,方便我们对该块内存的读写。
- 基本属性
- capacity
- 属性,容量,Buffer 能容纳的数据元素的最大值。这一容量在 Buffer 创建时被赋值,并且永远不能被修改。
- limit
- 属性,上限。
- 写模式下,代表最大能写入的数据上限位置,这个时候 limit 等于 capacity 。
- 读模式下,在 Buffer 完成所有数据写入后,通过调用 #flip() 方法,切换到读模式。此时,limit 等于 Buffer 中实际的数据大小。因为 Buffer 不一定被写满,所以不能使用 capacity 作为实际的数据大小。
- position
- position 属性,位置,初始值为 0 。
- 写模式下,每往 Buffer 中写入一个值,position 就自动加 1 ,代表下一次的写入位置。
- 读模式下,每从 Buffer 中读取一个值,position 就自动加 1 ,代表下一次的读取位置。( 和写模式类似 )
- mark
- 属性,标记,通过 #mark() 方法,记录当前 position ;通过 reset() 方法,恢复 position 为标记。
- 写模式下,标记上一次写位置。
- 读模式下,标记上一次读位置。
- 关系
- mark <= position <= limit <= capacity
- 创建Buffer
- 每个 Buffer 实现类,都提供了 #allocate(int capacity) 静态方法,帮助我们快速实例化一个 Buffer 对象。
- ByteBuffer 实际是个抽象类,返回的是它的基于堆内( Non-Direct )内存的实现类 HeapByteBuffer 的对象。
- 每个 Buffer 实现类,都提供了 #wrap(array) 静态方法,帮助我们将其对应的数组包装成一个 Buffer 对象。
- 和 #allocate(int capacity) 静态方法一样,返回的也是 HeapByteBuffer 的对象。
- 每个 Buffer 实现类,都提供了 #allocateDirect(int capacity) 静态方法,帮助我们快速实例化一个 Buffer 对象。
- 和 #allocate(int capacity) 静态方法不一样,返回的是它的基于堆外( Direct )内存的实现类 DirectByteBuffer 的对象。
- 向 Buffer 写入数据
- 每个 Buffer 实现类,都提供了 #put(…) 方法,向 Buffer 写入数据。
- 对于 Buffer 来说,有一个非常重要的操作就是,我们要讲来自 Channel 的数据写入到 Buffer 中。
- 在系统层面上,这个操作我们称为读操作,因为数据是从外部( 文件或者网络等 )读取到内存中。
- 通常在说 NIO 的读操作的时候,我们说的是从 Channel 中读数据到 Buffer 中,对应的是对 Buffer 的写入操作
- 从 Buffer 读取数据
- 每个 Buffer 实现类,都提供了 #get(…) 方法,从 Buffer 读取数据。
- 对于 Buffer 来说,还有一个非常重要的操作就是,我们要讲来向 Channel 的写入 Buffer 中的数据。
- 在系统层面上,这个操作我们称为写操作,因为数据是从内存中写入到外部( 文件或者网络等 )。
- rewind() flip() clear()
- flip
- 如果要读取 Buffer 中的数据,需要切换模式,从写模式切换到读模式。
- rewind
- 可以重置 position 的值为 0 。因此,我们可以重新读取和写入 Buffer 了。
- 该方法主要针对于读模式,所以可以翻译为“倒带”。
- clear
- 可以“重置” Buffer 的数据。因此,我们可以重新读取和写入 Buffer 了。
- 该方法主要针对于写模式。
- Buffer 的数据实际并未清理掉
- mark() 搭配 reset()
关于 Direct Buffer 和 Non-Direct Buffer 的区别
- Direct Buffer:
- 所分配的内存不在 JVM 堆上, 不受 GC 的管理.(但是 Direct Buffer 的 Java 对象是由 GC 管理的, 因此当发生 GC, 对象被回收时, Direct Buffer 也会被释放)
- 因为 Direct Buffer 不在 JVM 堆上分配, 因此 Direct Buffer 对应用程序的内存占用的影响就不那么明显(实际上还是占用了这么多内存, 但是 JVM 不好统计到非 JVM 管理的内存.)
- 申请和释放 Direct Buffer 的开销比较大. 因此正确的使用 Direct Buffer 的方式是在初始化时申请一个 Buffer, 然后不断复用此 buffer, 在程序结束后才释放此 buffer.
- 使用 Direct Buffer 时, 当进行一些底层的系统 IO 操作时, 效率会比较高, 因为此时 JVM 不需要拷贝 buffer 中的内存到中间临时缓冲区中.
- Non-Direct Buffer:
- 直接在 JVM 堆上进行内存的分配, 本质上是 byte[] 数组的封装.
- 因为 Non-Direct Buffer 在 JVM 堆中, 因此当进行操作系统底层 IO 操作中时, 会将此 buffer 的内存复制到中间临时缓冲区中. 因此 Non-Direct Buffer 的效率就较低.
Selector
- Selector , 一般称为选择器。它是 Java NIO 核心组件中的一个,用于轮询一个或多个 NIO Channel 的状态是否处于可读、可写。如此,一个线程就可以管理多个 Channel ,也就说可以管理多个网络连接。也因此,Selector 也被称为多路复用器。
- 那么 Selector 是如何轮询的呢?
- 首先,需要将 Channel 注册到 Selector 中,这样 Selector 才知道哪些 Channel 是它需要管理的。
- 之后,Selector 会不断地轮询注册在其上的 Channel 。如果某个 Channel 上面发生了读或者写事件,这个 Channel 就处于就绪状态,会被 Selector 轮询出来,然后通过 SelectionKey 可以获取就绪 Channel 的集合,进行后续的 I/O 操作。
- 优缺点
- 优点
- 使用一个线程能够处理多个 Channel 的优点是,只需要更少的线程来处理 Channel 。
- 事实上,可以使用一个线程处理所有的 Channel 。
- 对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源( 例如 CPU、内存 )。因此,使用的线程越少越好。
- 缺点
- 因为在一个线程中使用了多个 Channel ,因此会造成每个 Channel 处理效率的降低。
- 创建 Selector
- 通过 #open() 方法,我们可以创建一个 Selector 对象。代码如下:
- 注册 Chanel 到 Selector 中
- 为了让 Selector 能够管理 Channel ,我们需要将 Channel 注册到 Selector 中。
- 如果一个 Channel 要注册到 Selector 中,那么该 Channel 必须是非阻塞。
- FileChannel 是不能够注册到 Channel 中的,因为它是阻塞的。
- 监听四种不同类型的事件:
- Connect :连接完成事件( TCP 连接 ),仅适用于客户端,对应 SelectionKey.OP_CONNECT 。
- Accept :接受新连接事件,仅适用于服务端,对应 SelectionKey.OP_ACCEPT 。
- Read :读事件,适用于两端,对应 SelectionKey.OP_READ ,表示 Buffer 可读。
- Write :写时间,适用于两端,对应 SelectionKey.OP_WRITE ,表示 Buffer 可写。
- Channel 触发了一个事件,意思是该事件已经就绪:
- 一个 Client Channel Channel 成功连接到另一个服务器,称为“连接就绪”。
- 一个 Server Socket Channel 准备好接收新进入的连接,称为“接收就绪”。
- 一个有数据可读的 Channel ,可以说是“读就绪”。
- 一个等待写数据的 Channel ,可以说是“写就绪”。
- SelectionKey 类
- 调用 Channel 的 #register(…) 方法,向 Selector 注册一个 Channel 后,会返回一个 SelectionKey 对象。
- SelectionKey 在 java.nio.channels 包下,被定义成一个抽象类,表示一个 Channel 和一个 Selector 的注册关系。
- 注册关系,包含如下内容:
- interest set: 感兴趣的事件集合。
- ready set :就绪的事件集合。
- Channel
- Selector
- attachment :可选的附加对象。可以向 SelectionKey 添加附加对象。
- 通过 Selector 选择 Channel
- 在 Selector 中,提供三种类型的选择( select )方法,返回当前有感兴趣事件准备就绪的 Channel 数量。
- select() 阻塞到至少有一个 Channel 在你注册的事件上就绪了。
- select(long timeout) 在
#select()
方法的基础上,增加超时机制。
- selectNow() 和
#select()
方法不同,立即返回数量,而不阻塞。
- select 方法返回的 int 值,表示有多少 Channel 已经就绪。也就是自上次调用 select 方法后有多少 Channel 变成就绪状态。
- 获取可操作的 Channel
- 一旦调用了 select 方法,并且返回值表明有一个或更多个 Channel 就绪了,然后可以通过调用Selector 的 #selectedKeys() 方法,访问“已选择键集( selected key set )”中的就绪 Channel 。
- 注意,当有新增就绪的 Channel ,需要先调用 select 方法,才会添加到“已选择键集( selected key set )”中。否则,我们直接调用 #selectedKeys() 方法,是无法获得它们对应的 SelectionKey 们。
- 唤醒 Selector 选择
- 某个线程调用 #select() 方法后,发生阻塞了,即使没有通道已经就绪,也有办法让其从 #select() 方法返回。
- 只要让其它线程在第一个线程调用 select() 方法的那个 Selector 对象上,调用该 Selector 的 #wakeup() 方法,进行唤醒该 Selector 即可。
- 注意,如果有其它线程调用了 #wakeup() 方法,但当前没有线程阻塞在 #select() 方法上,下个调用 #select() 方法的线程会立即被唤醒。
- 关闭 Selector
- 当我们不再使用 Selector 时,可以调用 Selector 的 #close() 方法,将它进行关闭。
- Selector 相关的所有 SelectionKey 都会失效。
- Selector 相关的所有 Channel 并不会关闭。
- 此时若有线程阻塞在 #select() 方法上,也会被唤醒返回。
NIO与BIO相比
NIO
- 基于缓冲区
- 基于Buffer读取,将数据从Channel中读取到Buffer中,或者从buffer中将数据写回到channel中。因为数据已经读取到缓冲区当中,所以操作不需要顺序执行,增加其灵活性。
- 非阻塞IO
- 一个线程从channel中执行io操作的时候,无论是读取还是写入,都无需等待完成,都会直接返回,不会阻塞当前正在执行的线程。
- 有选择器
- 一个线程可以通过一个Selector管理多个Channel,选择器是实现非阻塞io的核心。
- Selector内部自动为我们实现了轮训select操作,判断channel是否有已经就绪的io事件(连接,读,写等)
BIO
- 基于流(Stream)
- 以流式方式进行处理,顺序的从一个stream中读取一个或者多个字节,直到读取完成。由于没有缓存区,不能随意更改读取指针的位置。
- 阻塞IO
- 一个线程操作io的时候,该线程会被阻塞,直到数据被读取或者写入完成。