Java NIO:Buffer、Channel 和 Selector

Buffer

一个 Buffer 本质上是内存中的一块,我们可以将数据写入这块内存,之后从这块内存获取数据。

java.nio 定义了以下几个 Buffer 的实现,这个图读者应该也在不少地方见过了吧。

其实核心是最后的 ByteBuffer,前面的一大串类只是包装了一下它而已,我们使用最多的通常也是 ByteBuffer。

我们应该将 Buffer 理解为一个数组,IntBuffer、CharBuffer、DoubleBuffer 等分别对应 int[]、char[]、double[] 等。

MappedByteBuffer 用于实现内存映射文件,也不是本文关注的重点。

我觉得操作 Buffer 和操作数组、类集差不多,只不过大部分时候我们都把它放到了 NIO 的场景里面来使用而已。下面介绍 Buffer 中的几个重要属性和几个重要方法。

position、limit、capacity

就像数组有数组容量,每次访问元素要指定下标,Buffer 中也有几个重要属性:position、limit、capacity。

最好理解的当然是 capacity,它代表这个缓冲区的容量,一旦设定就不可以更改。比如 capacity 为 1024 的 IntBuffer,代表其一次可以存放 1024 个 int 类型的值。一旦 Buffer 的容量达到 capacity,需要清空 Buffer,才能重新写入值。

position 和 limit 是变化的,我们分别看下读和写操作下,它们是如何变化的。

position 的初始值是 0,每往 Buffer 中写入一个值,position 就自动加 1,代表下一次的写入位置。读操作的时候也是类似的,每读一个值,position 就自动加 1。

从写操作模式到读操作模式切换的时候(flip),position 都会归零,这样就可以从头开始读写了。

limit:写操作模式下,limit 代表的是最大能写入的数据,这个时候 limit 等于 capacity。写结束后,切换到读模式,此时的 limit 等于 Buffer 中实际的数据大小,因为 Buffer 不一定被写满了。

初始化 Buffer

每个 Buffer 实现类都提供了一个静态方法 allocate(int capacity) 帮助我们快速实例化一个 Buffer。如:

ByteBuffer byteBuf = ByteBuffer.allocate(1024);
IntBuffer intBuf = IntBuffer.allocate(1024);
LongBuffer longBuf = LongBuffer.allocate(1024);

另外,我们经常使用 wrap 方法来初始化一个 Buffer。

public static ByteBuffer wrap(byte[] array) {
    ...
}

 

填充 Buffer

各个 Buffer 类都提供了一些 put 方法用于将数据填充到 Buffer 中,如 ByteBuffer 中的几个 put 方法:

// 填充一个 byte 值
public abstract ByteBuffer put(byte b);
// 在指定位置填充一个 int 值
public abstract ByteBuffer put(int index, byte b);
// 将一个数组中的值填充进去
public final ByteBuffer put(byte[] src) {...}
public ByteBuffer put(byte[] src, int offset, int length) {...}

上述这些方法需要自己控制 Buffer 大小,不能超过 capacity,超过会java.nio.BufferOverflowException 异常。  

对于 Buffer 来说,另一个常见的操作中就是,我们要将来自 Channel 的数据填充到 Buffer 中,在系统层面上,这个操作我们称为读操作,因为数据是从外部(文件或网络等)读到内存中。

int num = channel.read(buf);

上述方法会返回从 Channel 中读入到 Buffer 的数据大小。

提取 Buffer 中的值

前面介绍了写操作,每写入一个值,position 的值都需要加 1,所以 position 最后会指向最后一次写入的位置的后面一个,如果 Buffer 写满了,那么 position 等于 capacity(position 从 0 开始)。

如果要读 Buffer 中的值,需要切换模式,从写入模式切换到读出模式。注意,通常在说 NIO 的读操作的时候,我们说的是从 Channel 中读数据到 Buffer 中,对应的是对 Buffer 的写入操作,初学者需要理清楚这个。

调用Buffer的flip()方法,可以从写模式切换到读模式,其实就是重新设置了一下position和limit的值。

public final Buffer flip() {
    limit = position; // 将 limit 设置为实际写入的数据数量
    position = 0; // 重置 position 为 0
    mark = -1; // mark 之后再说
    return this;
}

对应写操作的一系列put方法,读操作提供了一系列的get()方法:

// 根据 position 来获取数据
public abstract byte get();
// 获取指定位置的数据
public abstract byte get(int index);
// 将 Buffer 中的数据写入到数组中
public ByteBuffer get(byte[] dst)

附一个经常使用的方法:

new String(buffer.array()).trim();

除了将数据从Buffer读取出来使用,更常见的操作是将写入的数据输出到Channel中,如通过FileChannel将数据写入到文件中,通过SocketChannel将数据写入到网络发送到远程机器等。对应的,这种操作,我们称之为写操作。

int num = channel.write(buf);

mark()、reset()

除了position、limit、capacity这三个基本属性外,还有一个常用的属性就是mark。

mark用于临时保存position的值,每次调用mark()方法都会将mark设置为当前的position,便于后学需要的时候使用。

public final Buffer mark() {
    mark = position;
    return this;
}

那到底什么时候用呢?考虑以下场景,我们在 position 为 5 的时候,先 mark() 一下,然后继续往下读,读到第 10 的时候,我想重新回到 position 为 5 的地方重新来一遍,那只要调一下 reset() 方法,position 就回到 5 了。

public final Buffer reset() {
    int m = mark;
    if (m < 0)
        throw new InvalidMarkException();
    position = m;
    return this;
}

rewind()、clear()、compact()

rewind():会重置position为0,通常用于从头读写Buffer。

public final Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
}

 clear():相当于重新实例化。

通常,我们会先填充Buffer,然后从Buffer读取数据,之后再重新往里填充新的数据,我们一般在填充之前先调用clear().

public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}

compact():和clear()一样的是都是在准备往Buffer中填充新数据之前调用。

clear()会重置几个属性,但是并不会将Buffer中的数据清空,只不过后面写的时候会覆盖之前的数据。

而compact()方法调用之后,会先处理还没有读取的数据,也就是position到limit直接的数据,先将这些数据都移动到左边,然后在这个基础之上再开始写入。此时,limit还是等于capacity,position指向原来数据的右边。

Channel

所有的 NIO 操作始于通道,通道是数据来源或数据写入的目的地,主要地,我们将关心 java.nio 包中实现的以下几个 Channel:

FileChannel:文件通道,用于文件的读和写。

DatagramChannel:用于UDP连接的接收和发送

SocketChannel:TCP客户端

ServerSocketChannel:TCP服务端,监听某个端口进来的请求。

Channel 经常翻译为通道,类似 IO 中的流,用于读取和写入。它与前面介绍的 Buffer 打交道,读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。

FileChannel

初始化:

FileInputStream inputStream = new FileInputStream(new File("/data.txt"));
FileChannel fileChannel = inputStream.getChannel();  

当然了,也可以从RandomAccessFile类中的getChannel来得到FileChannel。

读取文件内容:

ByteBuffer buffer = ByteBuffer.allocate(1024);

int num = fileChannel.read(buffer);

写入文件内容:

ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("随机写入一些内容到 Buffer 中".getBytes());
// Buffer 切换为读模式
buffer.flip();
while(buffer.hasRemaining()) {
    // 将 Buffer 中的内容写入文件
    fileChannel.write(buffer);
}

SocketChannel

打开一个TCP链接:

SocketChannel socketChannel = SocketChannel  .open(new InetSocketAddress("127.0.0.1", 80));

当然了,上面的这行代码等价于下面的两行:

// 打开一个通道
SocketChannel socketChannel = SocketChannel.open();
// 发起连接
socketChannel.connect(new InetSocketAddress("127.0.0.1", 80));

SocketChannel 的读写和 FileChannel 没什么区别,就是操作缓冲区。

// 读取数据
socketChannel.read(buffer);

// 写入数据到网络连接中
while(buffer.hasRemaining()) {
    socketChannel.write(buffer);
}

ServerSocketChannel

ServerSocketChannel 用于监听机器端口,管理从这个端口进来的 TCP 连接。

// 实例化
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 监听 8080 端口
serverSocketChannel.socket().bind(new InetSocketAddress(8080));

while (true) {
    // 一旦有一个 TCP 连接进来,就对应创建一个 SocketChannel 进行处理
    SocketChannel socketChannel = serverSocketChannel.accept();
}

这里我们看到了SocketChannel的第二个实例化方式。

到这里,我们应该能理解SocketChannel了,它不仅仅是TCP客户端,它代表的是一个网络通道,可读可写。

ServerSocketChannel不和Buffer打交道了,因为它并不实际处理数据,一旦接到请求,就会实例化一个SocketChannel,之后再这个简介通道上传递的数据它就不管了,它会继续监听端口等待下一个连接。

DatagramChannel

UDP 和 TCP 不一样,DatagramChannel 一个类处理了服务端和客户端。

UDP 是面向无连接的,不需要和对方握手,不需要通知对方,就可以直接将数据包投出去,至于能不能送达,它是不知道的.

监听端口:

DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9090));

ByteBuffer buf = ByteBuffer.allocate(48);

channel.receive(buf);

发送数据:

String newData = "New String to write to file..."
                    + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);
buf.put(newData.getBytes());
buf.flip();

int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80));

Selector

Selector建立在非阻塞的基础之上,大家经常听到的多路复用在java世界中指的就是它,用于实现一个线程管理多个Channel。

开启Selector:

Selector selector = Selector.open();

将 Channel 注册到 Selector 上。前面我们说了,Selector 建立在非阻塞模式之上,所以注册到 Selector 的 Channel 必须要支持非阻塞模式,FileChannel 不支持非阻塞,我们这里讨论最常见的 SocketChannel 和 ServerSocketChannel。

// 将通道设置为非阻塞模式,因为默认都是阻塞模式的
channel.configureBlocking(false);
// 注册
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

register 方法的第二个 int 型参数(使用二进制的标记位)用于表明需要监听哪些感兴趣的事件,共以下四种事件:

SelectionKey.OP_READ:对应 00000001,通道中有数据可以进行读取

SelectionKey.OP_WRITE:对应 00000100,可以往通道中写入数据

SelectionKey.OP_CONNECT:对应 00001000,成功建立 TCP 连接

SelectionKey.OP_ACCEPT:对应 00010000,接受 TCP 连接

我们可以同时监听一个 Channel 中的发生的多个事件,比如我们要监听 ACCEPT 和 READ 事件,那么指定参数为二进制的 00010001 即十进制数值 17 即可。

注册方法返回值是 SelectionKey 实例,它包含了 Channel 和 Selector 信息,也包括了一个叫做 Interest Set 的信息,即我们设置的我们感兴趣的正在监听的事件集合。

调用 select() 方法获取通道信息。用于判断是否有我们感兴趣的事件已经发生了。

示例:

Selector selector = Selector.open();

channel.configureBlocking(false);

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

while(true) {
  // 判断是否有事件准备好
  int readyChannels = selector.select();
  if(readyChannels == 0) continue;

  // 遍历
  Set<SelectionKey> selectedKeys = selector.selectedKeys();
  Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
  while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.

    } else if (key.isConnectable()) {
        // a connection was established with a remote server.

    } else if (key.isReadable()) {
        // a channel is ready for reading

    } else if (key.isWritable()) {
        // a channel is ready for writing
    }

    keyIterator.remove();
  }
}

对于Selector,需要熟悉以下几个方法:

select()

调用此方法,会将上次 select 之后的准备好的 channel 对应的 SelectionKey 复制到 selected set 中。如果没有任何通道准备好,这个方法会阻塞,直到至少有一个通道准备好。

selectNow()

功能和 select 一样,区别在于如果没有准备好的通道,那么此方法会立即返回 0。

select(long timeout)

看了前面两个,这个应该很好理解了,如果没有通道准备好,此方法会等待一会

wakeup()

这个方法是用来唤醒等待在 select() 和 select(timeout) 上的线程的。如果 wakeup() 先被调用,此时没有线程在 select 上阻塞,那么之后的一个 select() 或 select(timeout) 会立即返回,而不会阻塞,当然,它只会作用一次。

用 Buffer 的 flip() 方法,可以从写入模式切换到读取模式。其实这个方法也就是设置了一下 position 和 limit 值罢了

原文地址:https://www.cnblogs.com/weiqihome/p/9926490.html

时间: 2024-08-11 03:01:21

Java NIO:Buffer、Channel 和 Selector的相关文章

Java NIO(1)----Channel 和 Buffer

Java NIO 由以下几个核心部分组成: Channels Buffers Selectors 虽然Java NIO 中除此之外还有很多类和组件,但在我看来,Channel,Buffer 和 Selector 构成了核心的API.其它组件,如Pipe和FileLock,只不过是与三个核心组件共同使用的工具类.因此,在概述中我将集中在这三个组件上.其它组件会在单独的章节中讲到. Channel 和 Buffer 基本上,所有的 IO 在NIO 中都从一个Channel 开始.Channel 有点

Java NIO(六) Selector

Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件.这样,一个单独的线程可以管理多个channel,从而管理多个网络连接. 下面是本文所涉及到的主题列表: 为什么使用Selector? Selector的创建 向Selector注册通道 SelectionKey 通过Selector选择通道 wakeUp() close() 完整的示例 为什么使用Selector? 仅用单个线程来处理多个Channels的好处是,只需要更少的

java NIO-Channel

基本简介 Java NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java 1.4开始),Java NIO提供了与标准IO不同的IO工作方式. Java NIO: Channels and Buffers(通道和缓冲区) 标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中. Java NIO: Non-blocking IO(非阻塞IO) Java

java nio之 channel通道(二)

java nio 通道上一篇文章里就讲述过,channel总是写数据的时候,要先把数据写入到bytebuffer,读数据的时候总是要先从channel中读入到bytebuffer.如下图,这个图是好多知名博客常用的图,很好理解这个channel. channel分为一下几种: FileChannel SocketChannel ServerSocketChannel DatagramChannel FileChannel: 经常说的FileChannel都是拿下面的例子说事 代码如下: pack

java nio之channel

一.通道(Channel):由 java.nio.channels 包定义的.Channel 表示 IO 源与目标打开的连接.Channel 类似于传统的"流".只不过 Channel本身不能直接访问数据,Channel 只能与Buffer 进行交互. 二.Channel重要实现 FileChannel:操作文件的读写 SocketChannel:通过TCP读写网络数据 ServerSocketChannel:监听TCP连接,你能利用它创建一个最简单的Web服务器 DatagramCh

JAVA NIO buffer (知识三)

java nio 里的buffer是缓存数据,通常缓冲区是一个数组,字节数组,也可以是别的类型.最常用的就是bytebuffer, 还有一些其它的类型: charbuffer, shortbuffer, intbuffer, longbuffer, floatbuffer, doublebufer. 一开始在知识(一)里写到,想要用nio读取数据,都是从channel读取到buffer.然后应用从buffer读取数据,同样写数据也是,先把数据写到buffer中,然后读道channel中. 基本上

Java NIO Buffer

Java NIO中的Buffer用于和NIO通道进行交互.如你所知,数据是从通道读入缓冲区,从缓冲区写入到通道中的.交互图如下: 缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存.这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存. 下面是NIO Buffer相关的话题列表: 1.Buffer的基本用法 使用Buffer读写数据一般遵循以下四个步骤: 写入数据到Buffer 调用flip()方法 从Buffer中读取数据 调用clear()方法或者com

Java NIO (2) Channel

Java NIO Channel Java NIO Channels are similar to streams with a few differences: You can both read and write to a Channels. Streams are typically one-way (read or write). Channels can be read and written asynchronously. Channels always read to, or w

【JAVA】【NIO】4、Java NIO Buffer

Java NIO的Buffer用于和channel进行交互. buffer本质上是一个内存块,你可以写数据,然后读取出来. 这个内存块是通过NIO的Buffer对象进行包装的,该对象提供了一系列的方法,使得对内存块的访问更加容易了. 基本的Buffer使用 使用Buffer读写数据一般有如下4步: 1.将数据写入 Buffer 2.调用buffer.flip()方法 3.从Buffer中读出数据 4.调用buffer.clear()方法或buffer.compact()方法 当你将数据写入buf

Java NIO (5) Channel to Channel Transfers

Java NIO Channel to Channel Transfers In Java NIO you can transfer data directly from one channel to another, if one of the channels is a FileChannel. The FileChannel class has a transferTo() and a transferFrom() method which does this for you. trans