五、NIO教程—通道(Channel)和缓冲区(Buffer)讲解

    Java NIO (New IO) 是 Java 的替代 IO API,意思是标准Java IO和Java Networking API 的替代 。Java NIO 提供了与传统 IO API 不同的 IO 编程模型。注意:有时 NIO 被称为非阻塞 IO。然而,这并不是作者最初的意思,最初应该指的是New IO更为恰当一点。此外,部分 NIO API 实际上是阻塞的——例如文件 API——所以标签“非阻塞”会有点误导。

一、什么是非阻塞IO

1.1 非阻塞 IO

     Java NIO 使您能够进行非阻塞 IO。例如,线程可以要求通道将数据读入缓冲区。当通道将数据读入缓冲区时,线程可以做其他事情。一旦数据被读入缓冲区,线程就可以继续处理它。将数据写入通道也是如此。

通道和缓冲区
     在标准 IO API 中,您可以使用字节流和字符流。在 NIO 中,您可以使用通道和缓冲区。数据总是从通道读入缓冲区,或从缓冲区写入通道。

选择器
     Java NIO 包含“选择器”的概念。选择器是一个可以监视多个事件通道的对象(例如:连接打开、数据到达等)。因此,单个线程可以监视多个通道的数据。

1.2 NIO 概念

  • 频道
  • 缓冲器
  • 分散 - 聚集
  • 通道间传输
  • 选择器
  • 文件通道
  • 套接字通道
  • 服务器套接字通道
  • 非阻塞服务器设计
  • 数据报通道
  • 管道
  • NIO与 IO
  • 小路
  • 文件
  • 异步文件通道

二、NIO 概述

Java NIO 由以下核心组件组成:

  • 频道
  • 缓冲器
  • 选择器
    Java NIO 拥有比这些更多的类和组件,但在我看来Channel,Buffer 和Selector构成了 API 的核心。其余的组件,如 Pipe和FileLock只是与三个核心组件结合使用的实用程序类。因此,我将在本 NIO 概述中重点介绍这三个组件。其他组件在本教程的其他地方在它们自己的文本中进行了解释。

2.1 通道和缓冲区 Channels and Buffers

通常,NIO 中的所有 IO 都以Channel. AChannel有点像流。从Channel数据中可以读入一个Buffer. 数据也可以从Buffer到Channel中。这是一个例子:
在这里插入图片描述
Java NIO:Channels读数据到Buffers,Buffers写数据到Channels

有几种Channel和Buffer类型。以下是ChannelJava NIO 中主要实现的列表:

  • 文件通道 FileChannel
  • 数据报通道 DatagramChannel
  • 套接字通道 SocketChannel
  • 服务器套接字通道 ServerSocketChannel
    可以看到,这些通道涵盖了UDP+TCP网络IO和文件IO。

这些类也有一些有趣的接口,但为了简单起见,我将它们排除在本 Java NIO 概述之外。在本 Java NIO 教程的其他文本中,将在相关的地方解释它们。

以下是Buffer Java NIO中的核心实现列表:

  • 字节缓冲区 ByteBuffer
  • 字符缓冲区 CharBuffer
  • 双缓冲 DoubleBuffer
  • 浮动缓冲区 FloatBuffer
  • 内部缓冲区 IntBuffer
  • 长缓冲区 LongBuffer
  • 短缓冲区 ShortBuffer

这些Buffer涵盖了可以通过 IO 发送的基本数据类型:字节、短整型、整型、长整型、浮点型、双精度和字符。

Java NIO 也有一个MappedByteBuffer与内存映射文件结合使用。不过,我会Buffer在本概述中忽略这一点。

基本通道示例

这是一个使用FileChannel将一些数据读入Buffer的基本示例:

 RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
    FileChannel inChannel = aFile.getChannel();

    ByteBuffer buf = ByteBuffer.allocate(48);

    int bytesRead = inChannel.read(buf);
    while (bytesRead != -1) {

      System.out.println("Read " + bytesRead);
      buf.flip();

      while(buf.hasRemaining()){
          System.out.print((char) buf.get());
      }

      buf.clear();
      bytesRead = inChannel.read(buf);
    }
    aFile.close();

注意buf.flip()通话。首先你读入一个缓冲区。然后你翻转它。然后你读出来。我将在下一篇关于Buffer’s 的文章中详细介绍。

2.2 选择器 Selectors

一个Selector允许单个线程处理多个Channel’s。如果您的应用程序打开了许多连接(通道),但每个连接上的流量很低,这会很方便。例如,在聊天服务器中。

这是使用 一个 Selector处理 3个线程的Channel说明图:
在这里插入图片描述
Java NIO:一个线程使用一个选择器来处理 3 个通道的

要使用 Selector请求Channel使用它注册。然后你调用它的 select()方法。此方法将阻塞,直到为注册通道之一准备好事件。一旦方法返回,线程就可以处理这些事件。事件的例子是传入连接、接收到的数据等。

2.3 Buffer缓冲区

Java NIO 缓冲区在与 NIO 通道交互时使用。如您所知,数据从通道读取到缓冲区,然后从缓冲区写入通道。
缓冲区本质上是一块内存,您可以在其中写入数据,然后您可以再次读取这些数据。这个内存块被包装在一个 NIO Buffer 对象中,它提供了一组方法,可以更容易地使用内存块。

2.3.1 基本缓冲区使用

使用 一个Buffer读取和写入数据通常遵循以下 4 步过程:

  1. 将数据写入缓冲区
  2. 调用 buffer.flip()
  3. 从缓冲区中读取数据
  4. 调用buffer.clear()或buffer.compact()
    当您将数据写入缓冲区时,缓冲区会跟踪您已写入的数据量。一旦需要读取数据,就需要使用flip()方法调用将缓冲区从写入模式切换到读取模式。在读取模式下,缓冲区允许您读取写入缓冲区的所有数据。

读取完所有数据后,您需要清除缓冲区,使其准备好再次写入。您可以通过两种方式执行此操作:通过调用clear()或调用 compact(). 该clear()方法清除整个缓冲区。该compact() 方法仅清除您已读取的数据。任何未读数据都被移到缓冲区的开头,数据将在未读数据之后写入缓冲区。

这是一个简单的Buffer使用示例,写、翻转、读和清除操作以粗体显示:

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

//create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf); //read into buffer.
while (bytesRead != -1) {

  buf.flip();  //make buffer ready for read

  while(buf.hasRemaining()){
      System.out.print((char) buf.get()); // read 1 byte at a time
  }

  buf.clear(); //make buffer ready for writing
  bytesRead = inChannel.read(buf);
}
aFile.close();

2.3.2 缓冲容量、位置和限制

缓冲区本质上是一块内存,您可以在其中写入数据,然后您可以再次读取这些数据。这个内存块被包装在一个 NIO Buffer 对象中,它提供了一组方法,可以更容易地使用内存块。
一个Buffer具有您需要熟悉的三个属性,以便了解 a 的Buffer工作原理。这些是:

  • 容量 capacity
  • 位置 position
  • 限制 limit

position和limit 的含义取决于是否Buffer 在读或写模式。无论缓冲模式如何,容量始终是相同的。
这是写入和读取模式下的容量、位置和限制的说明。在插图之后的部分中进行了说明。
在这里插入图片描述
写入和读取模式下的缓冲区容量、位置和限制。

容量
作为一个内存块,aBuffer有一定的固定大小,也称为它的“容量”。您只能将capacity字节、长整型、字符等写入缓冲区。一旦 Buffer 已满,您需要先清空它(读取数据或清除它),然后才能将更多数据写入其中。

位置
当您将数据写入 时Buffer,您会在某个位置这样做。最初位置为​​ 0。当一个字节、长整数等被写入时,Buffer该位置前进以指向缓冲区中的下一个单元格以插入数据。位置可以最大限度地变成 capacity - 1。

当您从 a 读取数据时,Buffer您也是从给定位置读取数据。当您将 aBuffer从写入模式翻转 到读取模式时,位置将重置回 0。当您从 读取数据时,Buffer您会从读取数据position,并position 前进到下一个读取位置。

限制
在写入模式下,aBuffer的限制是您可以写入缓冲区的数据量的限制。在写入模式下,限制等于 的容量Buffer。

当切换Buffer到读取模式时,限制意味着您可以从数据中读取多少数据的限制。因此,当将 a 翻转Buffer为读取模式时,将限制设置为写入模式的写入位置。换句话说,您可以读取与写入一样多的字节(限制设置为写入的字节数,由位置标记)。

2.3.3 缓冲区类型

Java NIO 带有以下Buffer类型:

  • 字节缓冲区 ByteBuffer
  • 映射字节缓冲区 MappedByteBuffer
  • 字符缓冲区 CharBuffer
  • 双缓冲 DoubleBuffer
  • 浮动缓冲区 FloatBuffer
  • 内部缓冲区 IntBuffer
  • 长缓冲区 LongBuffer
  • 短缓冲区 ShortBuffer

如您所见,这些Buffer类型代表不同的数据类型。换句话说,它们让您可以将缓冲区中的字节作为 char、short、int、long、float 或 double 进行处理。
MappedByteBuffer的情况比较特殊,并在自己的文字覆盖。

2.3.4 分配缓冲区

要获得一个Buffer对象,你必须先分配它。每个Buffer类都有一个allocate()方法来做到这一点。下面是一个示例,显示了 如何分配, ByteBuffer容量为 48 字节:

ByteBuffer buf = ByteBuffer.allocate(48);

这是一个CharBuffer为 1024 个字符分配空间的示例:

CharBuffer buf = CharBuffer.allocate(1024);

2.3.5 将数据写入缓冲区

将数据写入缓冲区
您可以通过Buffer两种方式将数据写入 a :

  1. 将数据从aChannel写入aBuffer
  2. Buffer通过缓冲区的put()方法将数据写入自己。

下面是一个示例,展示了 aChannel如何将数据写入 a Buffer:

int bytesRead = inChannel.read(buf); //读入缓冲区。

这是一个Buffer通过put()方法将数据写入 a 的示例:

buf.put(127);    

该put()方法还有许多其他版本,允许您以Buffer多种不同方式将数据写入 . 例如,在特定位置写入,或将字节数组写入缓冲区。有关具体缓冲区实现的更多详细信息,请参阅 JavaDoc。

2.3.6 翻动 filp

该flip()方法Buffer从写入模式切换到读取模式。调用flip()将position返回设置为 0,并将返回设置为limit 刚刚所在的位置。

换句话说,position现在标记读取位置,并limit标记写入缓冲区的字节数、字符数等 - 可以读取的字节数、字符数等的限制。

2.3.7 从缓冲区读取数据

有两种方法可以从Buffer.

  1. 将缓冲区中的数据读入通道。
  2. 使用 get() 方法之一,自己从缓冲区读取数据。

以下是如何将数据从缓冲区读取到通道的示例:

//从缓冲区读取到通道。
int bytesWritten = inChannel.write(buf);

下面是一个Buffer使用 get() 方法从 a 读取数据的示例:

byte aByte = buf.get();

该get()方法还有许多其他版本,允许您以Buffer多种不同方式从中读取数据 。例如,在特定位置读取,或从缓冲区读取字节数组。有关具体缓冲区实现的更多详细信息,请参阅 JavaDoc。

2.3.8 倒带 rewind

该Buffer.rewind()套position回0,这样你就可以重新读取缓冲区中的所有数据。在limit保持不变,因此仍标记,可以从被读取许多元素(字节,字符等)如何Buffer。

2.3.9 清除和紧凑 clear/compact

完成读取数据后,Buffer您必须Buffer 准备再次写入。您可以通过调用clear()或调用 来实现compact()。
如果你调用clear()的position是设置回0,limit来 capacity。换句话说,Buffer清除了。中的数据Buffer 没有被清除。只有标记告诉您可以将数据写入的Buffer位置。

如果在Buffer调用clear()该数据时有任何未读数据将被“遗忘”,这意味着您不再有任何标记告诉您哪些数据已读取,哪些数据尚未读取。

如果 中还有未读数据,Buffer想稍后再读,但需要先写一些,调用compact()代替clear()。

compact()将所有未读数据复制到Buffer. 然后它设置position在最后一个未读元素之后。该limit属性仍然设置为capacity,就像clear()做的那样。现在Buffer已准备好写入,但您不会覆盖未读数据。

2.4 分散和聚集 Scatter / Gather

Java NIO 带有内置的分散/收集支持。分散/聚集是用于从通道读取和写入通道的概念。

从通道的分散读取是一种将数据读入多个缓冲区的读取操作。因此,通道将来自通道的数据“分散”到多个缓冲区中。

对通道的收集写入是一种将数据从多个缓冲区写入单个通道的写入操作。因此,通道将来自多个缓冲区的数据“收集”到一个通道中。

在您需要分别处理传输数据的各个部分的情况下,分散/收集非常有用。例如,如果消息由标题和正文组成,您可以将标题和正文保存在单独的缓冲区中。这样做可以让您更轻松地分别处理标题和正文。

2.4.1 分散读取 Scatter

“分散读取”将数据从单个通道读取到多个缓冲区中。这是该原则的说明:

下面是Scatter原理图:
Java NIO:分散读取
这是一个代码示例,展示了如何执行分散读取:

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);

注意缓冲区是如何首先插入到数组中的,然后将数组作为参数传递给 channel.read()方法。read()然后,该方法按照缓冲区在数组中出现的顺序从通道写入数据。一旦缓冲区已满,通道就会继续填充下一个缓冲区。

分散读取在进入下一个缓冲区之前填满了一个缓冲区这一事实意味着它不适合动态大小的消息部分。换句话说,如果您有标题和正文,并且标题是固定大小(例如 128 字节),则分散读取工作正常。

2.4.2 收集写作

“收集写入”将数据从多个缓冲区写入单个通道。这是该原则的说明:
Java NIO:收集写入
这是一个代码示例,展示了如何执行收集写入:

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

//write data into channel

ByteBuffer[] bufferArray = { header, body };

channel.write(bufferArray);

缓冲区数组被传递到write()方法中,该方法按照它们在数组中遇到的顺序写入缓冲区的内容。仅写入缓冲区的位置和限制之间的数据。因此,如果缓冲区的容量为 128 字节,但仅包含 58 字节,则只有 58 字节从该缓冲区写入通道。因此,与分散读取相比,聚集写入对动态大小的消息部分工作正常。

✌️✌️✌️码字不易,希望各位看官一键三连哈!💫💫💫

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猿小许

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值