`
bmladmin
  • 浏览: 30765 次
  • 性别: Icon_minigender_1
  • 来自: 四川成都
社区版块
存档分类
最新评论

NIO ByteBuffer 使用方法

    博客分类:
  • J2EE
 
阅读更多

缓冲区分配和包装

在能够读和写之前,必须有一个缓冲区,用静态方法 allocate() 来分配缓冲区:

ByteBuffer buffer = ByteBuffer.allocate(1024);

allocate() 方法分配一个具有指定大小的底层数组,并将它包装到一个缓冲区对象中 — 在本例中是一个 ByteBuffer。

还可以将一个现有的数组转换为缓冲区:

byte array[] = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap(array);

本例使用了 wrap() 方法将一个数组包装为缓冲区。一旦完成包装,底层数据就可以通过缓冲区或者直接访问。


缓冲区分片

slice() 方法根据现有的缓冲区创建一个 子缓冲区。也就是它创建一个新的缓冲区,新缓冲区与原来的缓冲区的一部分共享数据。

首先创建一个长度为 10 的 ByteBuffer:

ByteBuffer buffer = ByteBuffer.allocate(10);

然后使用数据来填充这个缓冲区,在第 n 个槽中放入数字 n:

for (int i = 0; i < buffer.capacity(); i++) {
     buffer.put((byte)i);
}

现在我们对这个缓冲区 分片,以创建一个包含槽 3 到槽 6 的子缓冲区。在某种意义上,子缓冲区就像原来的缓冲区中的一个 窗口 。

窗口的起始和结束位置通过设置 position 和 limit 值来指定,然后调用 Buffer 的 slice() 方法:

buffer.position(3);
buffer.limit(7);
ByteBuffer slice = buffer.slice();

片 是缓冲区的 子缓冲区 。不过,片段 和 缓冲区 共享同一个底层数据数组。

缓冲区份片和数据共享

遍历子缓冲区,将每一个元素乘以 11 来改变它。例如,5 会变成 55。

for (int i = 0; i < slice.capacity(); i++) {
     byte b = slice.get(i);
     b *= 11;
     slice.put(i, b);
}

原缓冲区中的内容:

buffer.position( 0 );
buffer.limit(buffer.capacity());

while (buffer.hasRemaining()) {
     System.out.println(buffer.get());
}

结果表明只有在子缓冲区窗口中的元素被改变了:

java SliceBuffer
0
1
2
33
44
55
66
7
8
9

缓冲区片对于促进抽象非常有帮助。可以编写自己的函数处理整个缓冲区,而且如果想要将这个过程应用于子缓冲区上,只需取主缓冲区的一个片,并将它传递给您的函数。这比编写自己的函数来取额外的参数以指定要对缓冲区的哪一部分进行操作更容易。

只读缓冲区

只读缓冲区非常简单 — 可以读取它们,但是不能向它们写入。可以通过调用缓冲区的 asReadOnlyBuffer() 方法,将任何常规缓冲区转换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区(并与其共享数据),只不过它是只读的。

只读缓冲区对于保护数据很有用。在将缓冲区传递给某个对象的方法时,您无法知道这个方法是否会修改缓冲区中的数据。创建一个只读的缓冲区可以 保证 该缓冲区不会被修改。

不能将只读的缓冲区转换为可写的缓冲区。

内存映射文件 I/O

内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。

内存映射文件 I/O 是通过使文件中的数据神奇般地出现为内存数组的内容来完成的。这其初听起来似乎不过就是将整个文件读到内存中,但是事实上并不是这样。一般来说,只有文件中实际读取或者写入的部分才会送入(或者 映射 )到内存中。

内存映射并不真的神奇或者多么不寻常。现代操作系统一般根据需要将文件的部分映射为内存的部分,从而实现文件系统。Java 内存映射机制不过是在底层操作系统中可以采用这种机制时,提供了对该机制的访问。

尽管创建内存映射文件相当简单,但是向它写入可能是危险的。仅只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。

将文件映射到内存

了解内存映射的最好方法是使用例子。在下面的例子中,要将一个 FileChannel (它的全部或者部分)映射到内存中。为此将使用 FileChannel.map() 方法。下面代码行将文件的前 1024 个字节映射到内存中:

MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024 );

map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行行映射。
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class UseMappedFile {
    private static final int start = 0;
    private static final int size = 1024;

    public static void main(String args[]) throws Exception {
        RandomAccessFile raf = new RandomAccessFile("C:\\usemappedfile.txt", "rw");
        FileChannel fc = raf.getChannel();

        MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, start, size);

        mbb.put(0, (byte)97);
        mbb.put(1023, (byte)122);

        raf.close();
    }
}

分散和聚集

分散/聚集 I/O 是使用多个而不是单个缓冲区来保存数据的读写方法。

一个分散的读取就像一个常规通道读取,只不过它是将数据读到一个缓冲区数组中而不是读到单个缓冲区中。同样地,一个聚集写入是向缓冲区数组而不是向单个缓冲区写入数据。

分散/聚集 I/O 对于将数据流划分为单独的部分很有用,这有助于实现复杂的数据格式。

分散/聚集 I/O

通道可以有选择地实现两个新的接口: ScatteringByteChannel 和 GatheringByteChannel。一个 ScatteringByteChannel 是一个具有两个附加读方法的通道:
long read(ByteBuffer[] dsts);
long read(ByteBuffer[] dsts, int offset, int length);

这些 long read() 方法很像标准的 read 方法,只不过它们不是取单个缓冲区而是取一个缓冲区数组。

在 分散读取 中,通道依次填充每个缓冲区。填满一个缓冲区后,它就开始填充下一个。在某种意义上,缓冲区数组就像一个大缓冲区。

分散/聚集 I/O 对于将数据划分为几个部分很有用。例如,您可能在编写一个使用消息对象的网络应用程序,每一个消息被划分为固定长度的头部和固定长度的正文。您可以创建一个刚好可以容纳头部的缓冲区和另一个刚好可以容难正文的缓冲区。当您将它们放入一个数组中并使用分散读取来向它们读入消息时,头部和正文将整齐地划分到这两个缓冲区中。

从缓冲区所得到的方便性对于缓冲区数组同样有效。因为每一个缓冲区都跟踪自己还可以接受多少数据,所以分散读取会自动找到有空间接受数据的第一个缓冲区。在这个缓冲区填满后,它就会移动到下一个缓冲区。

聚集写入

聚集写入 类似于分散读取,只不过是用来写入。它也有接受缓冲区数组的方法:
long write(ByteBuffer[] srcs);
long write(ByteBuffer[] srcs, int offset, int length);

聚集写对于把一组单独的缓冲区中组成单个数据流很有用。为了与上面的消息例子保持一致,您可以使用聚集写入来自动将网络消息的各个部分组装为单个数据流,以便跨越网络传输消息。


异步 I/O

异步 I/O 是一种 没有阻塞地 读写数据的方法。通常,在代码进行 read() 调用时,代码会阻塞直至有可供读取的数据。同样, write() 调用将会阻塞直至数据能够写入。

另一方面,异步 I/O 调用不会阻塞。相反,您将注册对特定 I/O 事件的兴趣 — 可读的数据的到达、新的套接字连接,等等,而在发生这样的事件时,系统将会告诉您。

异步 I/O 的一个优势在于,它允许您同时根据大量的输入和输出执行 I/O。同步程序常常要求助于轮询,或者创建许许多多的线程以处理大量的连接。使用异步 I/O,您可以监听任何数量的通道上的事件,不用轮询,也不用额外的线程。

异步 I/O 中的核心对象名为 Selector。Selector 就是您注册对各种 I/O 事件的兴趣的地方,而且当那些事件发生时,就是这个对象告诉您所发生的事件。

创建一个 Selector

Selector selector = Selector.open();

然后,将对不同的通道对象调用 register() 方法,以便注册我们对这些对象中发生的 I/O 事件的兴趣。register() 的第一个参数总是这个 Selector。

打开一个 ServerSocketChannel

为了接收连接,需要一个 ServerSocketChannel。事实上,要监听的每一个端口都需要有一个 ServerSocketChannel 。对于每一个端口,打开一个 ServerSocketChannel,如下所示:

ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking( false );

ServerSocket ss = ssc.socket();
InetSocketAddress address = new InetSocketAddress(ports[i]);
ss.bind( address );

第一行创建一个新的 ServerSocketChannel,最后三行将它绑定到给定的端口。第二行将 ServerSocketChannel 设置为 非阻塞的 。必须对每一个要使用的套接字通道调用这个方法,否则异步 I/O 就不能工作。

选择键

下一步是将新打开的 ServerSocketChannels 注册到 Selector上。为此使用 ServerSocketChannel.register() 方法,如下所示:

SelectionKey key = ssc.register(selector, SelectionKey.OP_ACCEPT);

register() 的第一个参数总是这个 Selector。第二个参数是 OP_ACCEPT,这里它指定我们想要监听 accept 事件,也就是在新的连接建立时所发生的事件。这是适用于 ServerSocketChannel 的唯一事件类型。

请注意对 register() 的调用的返回值。 SelectionKey 代表这个通道在此 Selector 上的这个注册。当某个 Selector 通知您某个传入事件时,它是通过提供对应于该事件的 SelectionKey 来进行的。SelectionKey 还可以用于取消通道的注册。

内部循环

现在已经注册了对一些 I/O 事件的兴趣,下面将进入主循环。使用 Selectors 的几乎每个程序都像下面这样使用内部循环:

int num = selector.select();

Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();

while (it.hasNext()) {
     SelectionKey key = (SelectionKey)it.next();
     // ... deal with I/O event ...
}

首先,调用 Selector 的 select() 方法。这个方法会阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时, select() 方法将返回所发生的事件的数量。

接下来,调用 Selector 的 selectedKeys() 方法,它返回发生了事件的 SelectionKey 对象的一个 集合 。

通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件。对于每一个 SelectionKey,必须确定发生的是什么 I/O 事件,以及这个事件影响哪些 I/O 对象。

监听新连接

程序执行到这里,仅注册了 ServerSocketChannel,并且仅注册它们“接收”事件。为确认这一点,对 SelectionKey 调用 readyOps() 方法,并检查发生了什么类型的事件:

if(key.isAcceptable()) {
     // Accept the new connection
}

可以肯定地说, readOps() 方法告诉我们该事件是新的连接。

接受新的连接

因为我们知道这个服务器套接字上有一个传入连接在等待,所以可以安全地接受它;也就是说,不用担心 accept() 操作会阻塞:

ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
SocketChannel sc = ssc.accept();

下一步是将新连接的 SocketChannel 配置为非阻塞的。而且由于接受这个连接的目的是为了读取来自套接字的数据,所以我们还必须将 SocketChannel 注册到 Selector上,如下所示:

sc.configureBlocking(false);
SelectionKey newKey = sc.register(selector, SelectionKey.OP_READ);

注意使用 register() 的 OP_READ 参数,将 SocketChannel 注册用于 读取 而不是 接受 新连接。

删除处理过的 SelectionKey

在处理 SelectionKey 之后,我们几乎可以返回主循环了。但是我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。如果我们没有删除处理过的键,那么它仍然会在主集合中以一个激活的键出现,这会导致我们尝试再次处理它。我们调用迭代器的 remove() 方法来删除处理过的 SelectionKey:

it.remove();

现在我们可以返回主循环并接受从一个套接字中传入的数据(或者一个传入的 I/O 事件)了。

传入的 I/O

当来自一个套接字的数据到达时,它会触发一个 I/O 事件。这会导致在主循环中调用 Selector.select(),并返回一个或者多个 I/O 事件。这一次, SelectionKey 将被标记为 OP_READ 事件,如下所示:

} else if (key.isReadable()) {
     // Read the data
     SocketChannel sc = (SocketChannel)key.channel();
}

与以前一样,我们取得发生 I/O 事件的通道并处理它。

回到主循环

每次返回主循环,都要调用 select 的 Selector()方法,并取得一组 SelectionKey。每个键代表一个 I/O 事件。我们处理事件,从选定的键集中删除 SelectionKey,然后返回主循环的顶部。

这个程序有点过于简单,因为它的目的只是展示异步 I/O 所涉及的技术。在现实的应用程序中,您需要通过将通道从 Selector 中删除来处理关闭的通道。而且您可能要使用多个线程。这个程序可以仅使用一个线程,因为它只是一个演示,但是在现实场景中,创建一个线程池来负责 I/O 事件处理中的耗时部分会更有意义

分享到:
评论

相关推荐

    深入理解Apache Mina (6)---- Java Nio ByteBuffer与Mina ByteBuffer的区别

    5. 高效的API:Mina ByteBuffer的API设计更贴近网络编程需求,提供了如writeInt、readUTF等方法,使得数据序列化和反序列化更便捷。 总的来说,Mina ByteBuffer是对Java NIO ByteBuffer的扩展和优化,它更加关注...

    NIO(byteBuffer)按行读取文件

    使用nio byteBuffer 实现按行读取文件(大文件) 在window/linux/macOS上均测试通过 对于中文乱码也已处理成功 完整注释,可随需求更改 有问题请邮件:mly610865580@126.com

    Java NIO学习笔记——ByteBuffer用法

    本文主要关注的是Java NIO中的ByteBuffer,一个关键的数据容器,用于在通道(Channel)和缓冲区(Buffer)之间传输数据。ByteBuffer的用法是Java NIO学习中的核心内容。 首先,我们了解下ByteBuffer的基本概念。...

    Android在JNI中使用ByteBuffer的方法

    本文实例讲述了Android在JNI中使用ByteBuffer的方法。分享给大家供大家参考。具体如下: 一、ByteBuffer 定义 在NIO中,数据的读写操作始终是与缓冲区相关联的(读取时信道(SocketChannel)将数据读入缓冲区,写入时...

    Java NIO 主要类和方法(Java NIO中文版 附录C)

    在Java NIO中,Buffer类是所有其他缓冲类的基类,为所有缓冲器类型提供了通用方法。下面详细说明Java NIO中的主要类和方法。 Buffer类(C.1.1): Buffer类是一个抽象类,提供了缓冲区操作的基本结构。以下是一些...

    java NIO原理和使用

    接着,我们分配了一个 `ByteBuffer` 并使用 `read()` 和 `write()` 方法来完成文件的复制操作。 通过上述介绍和示例,我们可以看出 Java NIO 在提高 I/O 性能方面具有显著优势,尤其是在处理大量数据时更为明显。...

    JAVA NIO 按行读取大文件支持 GB级别-修正版

    由于对于本程序 116个字节以上的行才有意义,所以 在next实现方法中,有对 116 长度的判断,否则返回 null 修正了之前版本中的问题: 修正后的方法 private int readByte() throws IOException{ fbb.rewind(); ...

    Android中的ByteBuffer解析

    ByteBuffer是Java NIO(Non-Blocking I/O)的一部分,允许开发者高效地读写大量原始字节,如处理图像、音频或网络数据。本文将深入探讨Android中ByteBuffer的工作原理及其常见应用场景。 一、ByteBuffer的基础概念 ...

    基于netty的nio使用demo源码

    Netty提供了一种方便的方式来处理这些操作,例如,使用read()方法从通道读取数据到缓冲区,然后通过处理器链进行解码;反之,使用write()和flush()方法将数据从缓冲区写入通道。 6. **关闭操作**:当不再需要连接时...

    java nio 实现socket

    在探讨如何使用Java NIO实现Socket通信之前,我们需要先理解NIO(Non-blocking I/O,非阻塞I/O)与传统阻塞I/O之间的区别。 **传统阻塞I/O模型**:在传统的Java IO编程中,当我们调用`read()`或`write()`方法时,...

    浅谈java中nio的使用方式

    使用NIO,一个线程就可以通过Selector来管理多个Channel,显著降低了线程创建和上下文切换的开销。 以下是一个简单的NIO服务器示例,展示了如何使用Selector、Channel和Buffer处理客户端连接: ```java public ...

    基于Groovy的NIO框架,仅供学习Java NIO使用。.zip

    Java NIO提供了ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer和LongBuffer等类型。 3. **选择器(Selectors)**:选择器用于监视多个通道的状态,当通道准备就绪进行读写操作时,选择器会通知我们...

    java nio Selector的使用-客户端

    在创建Selector时,可以使用`java.nio.channels.Selector.open()`方法。接着,我们可以使用`channel.configureBlocking(false)`将通道设置为非阻塞模式,然后调用`channel.register(selector, interestOps)`方法将...

    Java.NIO资源下载资源下载

    - **Java 正则表达式 API**:详细讲解了 Java 提供的正则表达式 API 及其使用方法。 - **字符串类的正则表达式方法**:讨论了 String 类中与正则表达式相关的内置方法。 - **面向对象的文件 grep**:通过一个具体的...

    NIO 入门.chm,NIO 入门.chm

    Java NIO提供了多种类型的缓冲区,如ByteBuffer、CharBuffer、IntBuffer、FloatBuffer等,每种缓冲区对应不同的数据类型。 3. **选择器(Selector)**:选择器是NIO中的多路复用器,它允许单个线程监控多个通道,当...

    Android开发进阶之NIO非阻塞包[定义].pdf

    与传统的InputStream和OutputStream相比,NIO的ByteBuffer允许非阻塞的数据传输,这意味着方法调用会立即返回,而不是等待数据完全传输完毕。 在处理字符串时,NIO的CharsetEncoder和CharsetDecoder类扮演了重要...

    NIO复制文件

    在文件复制中,我们通常使用`transferTo()`和`transferFrom()`方法。 2. **ByteBuffer**:`java.nio.ByteBuffer`是NIO中最重要的缓冲区类型,它可以被映射到文件,也可以作为网络I/O的数据容器。在复制文件时,我们...

    java NIO.zip

    Java NIO提供了诸如ByteBuffer、CharBuffer、IntBuffer等类型,对应于不同的数据类型。缓冲区具有读写位置,可以通过flip()、clear()和rewind()等方法来管理缓冲区的状态,从而高效地进行数据读写。 3. **选择器...

    java nio 读文件

    1. 高效:NIO使用缓冲区进行数据传输,减少了系统调用,提高了性能。 2. 非阻塞:在等待数据时,线程可以做其他工作,避免了阻塞I/O可能导致的资源浪费。 3. 多通道:通过选择器,可以同时监控多个通道,方便处理...

Global site tag (gtag.js) - Google Analytics