原文:http://tutorials.jenkov.com/java-nio/index.html
译文:http://ifeve.com/overview

1. Overview

NIO 由以下核心组件组成:

  • Channels
  • Buffers
  • Selectors

虽然 NIO 拥有比这些更多的类和组件,但在我看来 Channel,Buffer 和Selector 构成了 API 的核心,其余的组件,如 Pipe 和 FileLock 只是与三个核心组件结合使用的工具类

1.1 Channels 和 Buffers

通常,所有的 IO 在 NIO 中都从一个 Channel 开始,Channel 有点像流,可以从 Channel 读取数据到 Buffer,也可以从 Buffer 写入数据到 Channel

image.png

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

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

如你所见,这些通道涵盖了 UDP 和 TCP、网络 IO、文件 IO

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

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

这些 Buffer 涵盖了可以通过 IO 发送的基本数据类型:byte,short,int,long,float,double 和 char

NIO 还有一个 MappedByteBuffer,是和内存映射文件一起使用的,不过,我会在本概述中忽略这一点

1.2 Selectors

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

下面是一个线程使用 Selector 处理 3 个 Channel 的示例:

image.png

要使用 Selector,你需要向 Selector 注册 Channel,然后调用它的 select() 方法,该方法将一直阻塞,直到某个注册的 Channel 有事件就绪,一旦方法返回,线程就可以处理这些事件,比如有新连接进来,或者数据接收等

2. Channel

NIO 的 Channel 类似于流,但有一些不同:

  • Channel 可以读取和写入数据,但流通常是单向的(读或写)
  • Channel 可以异步读取和写入
  • Channel 中的数据总是要先读到一个 Buffer,或者总是要从一个 Buffer 中写入

如上所述,从 Channel 读取数据到 Buffer,并将数据从 Buffer 写入到 Channel,如下图所示

image.png

2.1 Channel 实现

以下是 Java NIO 中最重要的 Channel 实现:

  • FileChannel:从文件中读写数据
  • DatagramChannel:通过 UDP 在网络上读写数据
  • SocketChannel:通过 TCP 在网络上读写数据
  • ServerSocketChannel:允许你监听传入的 TCP 连接,就像 Web 服务器一样,对于每个传入的连接,都会创建一个 SocketChannel

2.2 基本 Channel 示例

try (FileChannel inputChannel = new RandomAccessFile("C:\\Users\\Administrator\\Downloads\\chenkaixin12121.txt", "rw").getChannel();) {
    // 创建容量为48字节的缓冲区
    ByteBuffer buf = ByteBuffer.allocate(48);
    // 读入缓冲区
    while (inputChannel.read(buf) != -1) {
        // 使缓冲区为读取做好准备
        buf.flip();
        // 输出缓冲区的内容
        System.out.print(StandardCharsets.UTF_8.decode(buf));
        // 使缓冲区为写入做好准备
        buf.clear();
    }
} catch (IOException e) {
    e.printStackTrace();
}

注意 buf.flip() 的调用,首先读取数据到 Buffer,然后反转 Buffer,接着再从 Buffer 中读取数据

3. Buffer

Buffer 是与 Chanenl 交互时使用的,如您所知,数据从 Channel 读取到 Buffer,又从 Buffer 写入到 Channel

缓冲区本质上是一个内存块,您可以在其中写入数据,然后可以再次读取数据,这个内存块包装在一个 Buffer 对象中,该对象提供了一组方法,使处理内存块更加容易

3.1 Buffer 的基本使用

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

  • 将数据写入 Buffer
  • 调用 buffer.flip()
  • 从 Buffer 中读取数据
  • 调用 buffer.clear() 或 buffer.compact()

当向 Buffer 写入数据时,Buffer 会记录下写了多少数据,一旦要读取数据,需要通过 flip() 方法将 Buffer 从写模式切换到读模式,在读模式下,可以读取之前写入到 Buffer 的所有数据

一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入,有两种方式能清空缓冲区:调用 clear() 或 compact() 方法,clear() 方法会清空整个缓冲区,compact() 方法只会清除已经读过的数据,未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面

3.2 Buffer 的 capacity、position 和 limit

为了理解 Buffer 是如何工作的,你需要熟悉 Buffer 的三个属性

  • capacity
  • position
  • limit

image.png

3.2.1 capacity

作为一个内存块,Buffer 有一个的固定大小,也称为 capacity,你只能往里写 capacity 个 byte、long、char 等类型,一旦 Buffer 满了,你需要清空它(读取数据或清除数据),然后才能继续写入数据

3.2.2 position

当你将数据写入 Buffer 时,position 表示当前的位置,初始的 position 值为 0,当一个 byte、long、char 等数据写到 Buffer 后,position 会向前移动到下一个可插入数据的 Buffer 单元,position 最大可为 capacity – 1

当读取数据时,也是从某个特定位置读,当将 Buffer 从写模式切换到读模式,position 会被重置为 0,当从 Buffer 的 position 处读取数据时,position 将向前移动到下一个可读的位置

3.2.3 limit

在写模式下,Buffer 的 limit 表示你最多能往 Buffer 里写多少数据, 写模式下,limit 等于 Buffer 的 capacity

当切换 Buffer 到读模式时,limit 表示你最多能读到多少数据,因此,当切换 Buffer 到读模式时,limit 会被设置成写模式下的 position 值,换句话说,你能读到之前写入的所有数据(limit 被设置成已写数据的数量,这个值在写模式下就是 position)

3.3 Buffer 的类型

Java NIO 有以下 Buffer 类型:

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

3.4 Buffer 的分配

要获得一个 Buffer 对象,你必须先分配它,每个 Buffer 类都有一个allocate() 方法可以做到这一点,下面是一个分配 48 字节的 ByteBuffer 的示例:

ByteBuffer buf = ByteBuffer.allocate(48);

3.5 将数据写入到 Buffer

你可以通过两种方式将数据写入到 Buffer

  • 将数据从 Channel 写入到 Buffer
  • 通过 Buffer 的 put() 方法将数据写入到 Buffer

这是一个示例,展示了 Channel 如何将数据写入到 Buffer:

int bytesRead = inChannel.read(buf);

通过 put() 方法:

buf.put(127);

put() 方法有很多版本,允许你以不同的方式将数据写入到 Buffer中,例如, 写入到一个指定的位置,或者把一个字节数组写入到 Buffer,更多 Buffer 实现的细节参考 JavaDoc

3.6 flip

flip() 方法将 Buffer 从写入模式切换到读取模式,调用 flip() 方法会将 position 设置为 0,并将 limit 设置为之前 position 的值

换句话说,position 现在标记读取位置,limit 标记写入 Buffer 的 byte、char 等 - 现在可以读取的 byte、char

3.7 从 Buffer 中读取数据

有两种方法可以从 Buffer 读取数据:

  • 从 Buffer 读取数据到 Channel
  • 使用 get() 方法从 Buffer 中读取数据

以下是从 Buffer 读取数据到 Channel 的例子:

int bytesWritten = inChannel.write(buf);

使用 get() 方法从 Buffer 中读取数据的例子:

byte aByte = buf.get();

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

3.8 rewind()

buffer.rewind()将 position 设置回 0,这样你就可以重新读取 Buffer 中的所有数据,limit 保持不变,因此仍然能表示从 Buffer 中读取多少元素(byte、char等)

3.9 clear() 和 compact()

一旦你完成了从 Buffer 中读取数据,你必须让 Buffer 为再次写入做好准备,可以通过调用 clear() 或 compact() 来实现

如果调用 clear(),position 将被重新设置为 0,而 limit 被设置为 capacity 的值,换句话说,Buffer 被清空,Buffer 中的数据未被清除,只是这些标记告诉我们可以从哪里开始往 Buffer 里写数据,

如果Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”,意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有,

如果 Buffer 中仍有未读的数据,且后续还需要这些数据,但是此时想要先先写些数据,那么使用 compact() 方法,

compact() 方法将所有未读的数据拷贝到 Buffer 起始处,然后将 position 设到最后一个未读元素正后面,limit 属性依然像 clear() 方法一样,设置成capacity,现在 Buffer 准备好写数据了,但是不会覆盖未读的数据

3.10 mark() 和 reset()

可以通过调用 Buffer.mark() 方法标记 Buffer 中的一个特定的 position,然后可以通过调用 Buffer.reset() 方法恢复到这个 position,下面是一个例子:

buffer.mark();

// call buffer.get() a couple of times, e.g. during parsing.

buffer.reset();  // set position back to mark.   

3.11 equals() 和 compareTo()

可以使用 equals() 和 compareTo() 比较两个 Buffer

3.11.1 equals()

当满足下列条件时,表示两个 Buffer 相等:

  • 它们的类型相同(byte、char、int 等)
  • Buffer 中剩余的 byte、char 等的个数相等
  • Buffer 中所有剩余的 byte、char 等都相同

如你所见,equals() 只是比较 Buffer 的一部分,不是每一个在它里面的元素都比较,实际上,它只比较 Buffer 中的剩余元素

3.11.2 compareTo()

compareTo() 方法比较两个 Buffer 的剩余元素(byte、char等), 如果满足下列条件,则认为一个 Buffer “小于” 另一个 Buffer:

  • 第一个不相等的元素小于另一个 Buffer 中对应的元素
  • 所有元素都相等,但第一个 Buffer 比另一个先耗尽(第一个 Buffer 的元素个数比另一个少)

剩余元素是从 position 到 limit 之间的元素

4. Scatter / Gather

NIO 带有内置的分散/聚集(Scatter / Gather)支持,分散/聚集是用于从 Channel 读取和写入 Channel 的概念

分散:是指读操作时将读取的数据写入到多个 Buffer
聚集:是指写操作时将多个 Buffer 的数据写入同一个 Channel

在需要分别处理传输数据的各个部分的情况下,分散/聚集非常有用,例如,如果消息是由消息头和消息体组成,你可以将消息头和消息体保存在单独的 Buffer 中,这样做可以更轻松地分别处理消息头和消息体

4.1 Scattering Reads

Scattering Reads 是指数据从一个 Channel 读取到多个 Buffer 中,如下图描述:

image.png

下面是一个代码示例:

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

// write data into channel

ByteBuffer[] bufferArray = { header, body };

channel.read(bufferArray);

注意 Buffer 首先被插入到数组,然后再将数组作为 channel.read() 的输入参数,read() 方法按照 Buffer 在数组中的顺序将从 Channel 中读取的数据写入到 Buffer,当一个 Buffer 被写满后,Channel 紧接着向另一个 Buffer
中写

Scattering Reads 在移动下一个 Buffer 前,必须填满当前的 Buffer,这也意味着它不适用于动态消息(消息大小不固定),换句话说,如果存在消息头和消息体,消息头必须完成填充(例如 128 byte),Scattering Reads 才能正常工作

4.2 Gathering Writes

Gathering Writes 是指数据从多个 Buffer 写入到同一个 Channel,如下图描述:

image.png

代码示例如下:

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

// write data into channel

ByteBuffer[] bufferArray = { header, body };

channel.write(bufferArray);

Buffer 数组是 write() 方法的入参,write() 方法会按照 Buffer 在数组中的顺序,将数据写入到 Channel,注意只有 position 和 limit 之间的数据才会被写入,因此,如果一个 Buffer 的容量为 128 byte,但是仅仅包含 58 byte 的数据,那么这 58 byte 的数据将被写入到 Channel 中,因此与Scattering Reads 相反,Gathering Writes 能较好的处理动态消息

5. Channel to Channel Transfers

在 Java NIO 中,如果两个 Channel 其中一个 Channel 是 FileChannel,那么你可以使用 transferTo() 和 transferFrom() 将数据直接从一个 Channel 传输到另一个 Channel

5.1 transferFrom()

该 FileChannel.transferFrom() 方法将数据从源通道传输到 FileChannel,这是一个简单的例子:

try (FileChannel fromChannel = new RandomAccessFile("C:\\Users\\Administrator\\Downloads\\test1.txt", "rw").getChannel();
     FileChannel toChannel = new RandomAccessFile("C:\\Users\\Administrator\\Downloads\\test2.txt", "rw").getChannel()) {
    long position = 0;
    long count = fromChannel.size();
    toChannel.transferFrom(fromChannel, position, count);
} catch (IOException e) {
    e.printStackTrace();
}

参数 position 表示从 position 处开始向目标文件写入数据,count 表示最多传输的字节数,如果源通道的剩余空间小于 count 个字节,则所传输的字节数要小于请求的字节数,

此外要注意,在 SoketChannel 的实现中,SocketChannel 只会传输此刻准备好的数据(可能不足 count 字节),因此,SocketChannel 可能不会将请求的所有数据(count 个字节)全部传输到 FileChannel 中

5.2 transferTo()

transferTo() 方法将数据从 FileChannel 传输到其他的 Channel 中,下面是一个简单的例子:

try (FileChannel fromChannel = new RandomAccessFile("C:\\Users\\Administrator\\Downloads\\test1.txt", "rw").getChannel();
     FileChannel toChannel = new RandomAccessFile("C:\\Users\\Administrator\\Downloads\\test2.txt", "rw").getChannel()) {
    long position = 0;
    long count = fromChannel.size();
    fromChannel.transferTo(position, count, toChannel);
} catch (IOException e) {
    e.printStackTrace();
}

请注意该示例与前一个示例的相似程度,除了调用方法的 FileChannel 对象不一样外,其余的都是一样的,

该 SocketChannel 的问题也存在于 transferTo() 方法中,该 SocketChannel 会一直传输字节直到目标 Buffer 会填满,然后停止

6. Selector

Selector 是一个组件,它可以检查一个或多个 Java NIO Channel 实例,并确定哪些 Channel 准备好进行读取或写入等操作,通过这种方式,单个线程可以管理多个 Channel,从而管理多个网络连接

6.1 为什么要使用 Selector

仅使用单个线程来处理多个 Channel 的优点是你需要更少的线程来处理通道,实际上,你可以只使用一个线程来处理您的所有 Channel,对于操作系统来说,线程之间的切换是昂贵的,并且每个线程也占用操作系统中的一些资源(内存),因此,使用的线程越少越好

但请记住,现代操作系统和 CPU 在多任务处理方面变得越来越好,因此多线程的开销会随着时间的推移而变小,事实上,如果一个 CPU 有多个内核,那么不进行多任务处理可能会浪费 CPU 能力,无论如何,该设计讨论属于不同的文章,在这里可以说,您可以使用单个线程用 Selector 处理多个 Channel

6.2 创建 Selector

您可以通过调用 Selector.open() 方法来创建一个 Selector ,如下所示:

Selector selector = Selector.open();

6.3 向 Selector 注册 Channel

为了使用 Channel 与 Selector,你必须在 Selector 中注册 Channel,使用 SelectableChannel.register() 方法来完成 ,如下所示:

channel.configureBlocking(false);

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

和 Selector 一起使用时,Channel 必须处于非阻塞模式下,这意味着您不能将 FileChannel 与 Selector 一起使用,因为 FileChannel 不能切换到非阻塞模式,不过,套接字通道可以正常工作,

注意该 register() 方法的第二个参数,这是一个“interest 集合”,意思是通过 Selector 监听 Channel 时对什么事件感兴趣,您可以监听四种不同的事件:

  • Connect
  • Accept
  • Read
  • Write

通道触发了一个事件意思是该事件已经就绪,所以,某个 Channel 成功连接到另一个服务器称为“连接就绪”,一个 ServerSocketChannel 准备好接收新进入的连接称为“接收就绪”,一个有数据可读的通道可以说是“读就绪”,等待写数据的通道可以说是“写就绪”

这四种事件用 SelectionKey 的四个常量来表示:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

如果您对多个事件感兴趣,用“位或”操作符将常量连接起来,如下:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;    

6.4 SelectionKey

正如您在上一节中看到的,当向 Selector 注册 Channel 时,该 register() 方法返回一个 SelectionKey 对象,这个 SelectionKey 对象包含一些有趣的属性:

  • interest 集合
  • ready 集合
  • Channel
  • Selector
  • 附加的对象(可选)
6.4.1 interest 集合

就像向 Selector 注册通道一节中所描述的,interest 集合是你所选择的感兴趣的事件集合,可以通过 SelectionKey 读写 interest 集合,像这样:

int interestSet = selectionKey.interestOps();

boolean isInterestedInAccept = SelectionKey.OP_ACCEPT == (interests & SelectionKey.OP_ACCEPT);
boolean isInterestedInConnect = SelectionKey.OP_CONNECT == (interests & SelectionKey.OP_CONNECT);
boolean isInterestedInRead = SelectionKey.OP_READ == (interests & SelectionKey.OP_READ);
boolean isInterestedInWrite = SelectionKey.OP_WRITE == (interests & SelectionKey.OP_WRITE);
6.4.2 ready 集合

ready 集合是通道已经准备就绪的操作的集合,在一次选择(Selection)之后,你会首先访问这个 ready set,Selection 将在下一小节进行解释,可以这样访问 ready 集合:

int readySet = selectionKey.readyOps();

可以用像检测 interest 集合那样的方法,来检测 Channel 中什么事件或操作已经就绪,但是,也可以使用以下四个方法,它们都会返回一个布尔类型:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
6.4.3 Channel + Selector

从 SelectionKey 访问 Channel 和 Selector 很简单,如下:

Channel  channel  = selectionKey.channel();

Selector selector = selectionKey.selector();   
6.4.4 附加的对象

可以将一个对象或者更多信息附着到 SelectionKey 上,这样就能方便的识别某个给定的通道,例如,可以附加与通道一起使用的 Buffer,或是包含聚集数据的某个对象,使用方法如下:

selectionKey.attach(theObject);

Object attachedObj = selectionKey.attachment();

还可以在使用 register() 方法向 Selector 注册 Channel 的时候附加对象,如:

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

6.5 通过 Selector 选择 Channel

一旦您用 Selector 注册了一个或多个通道,您就可以调用几个重载的 select() 方法,这些方法返回为您感兴趣的事件(连接、接受、读取或写入)“准备好”的 Channel,换句话说,如果您对“读就绪”的 Channel 感兴趣,select() 方法会返回读事件已经就绪的那些通道

下面是 select() 方法:

  • int select():阻塞到至少有一个通道在你注册的事件上就绪了
  • int select(long timeout):作用与 select() 相同,只是它会阻塞一个最大的超时毫秒
  • int selectNow():根本不会阻塞,立即返回任何准备好的通道

select() 方法返回的 int 值告诉我们有多少通道已经就绪,也就是说,自上次调用 select() 以来,有多少通道已经就绪,如果你调用 select(),它返回 1,因为有一个通道已经准备好了,如果再次调用 select(),又有一个通道就绪了,它会再次返回 1,如果对第一个就绪的 Channel 没有做任何操作,现在就有两个就绪的通道,但在每次 select() 方法调用之间,只有一个通道就绪了

6.5.1 selectedKeys()

一旦调用了 select() 方法,并且返回值表明有一个或更多个通道就绪了,然后可以通过调用 selector 的 selectedKeys() 方法,访问“已选择键集(selected key set)”中的就绪通道,如下所示:

Set<SelectionKey> selectedKeys = selector.selectedKeys();

当像 Selector 注册 Channel 时,Channel.register() 方法会返回一个 SelectionKey 对象,这个对象代表了注册到该 Selector 的通道,可以通过 SelectionKey 的 selectedKeySet() 方法访问这些对象,

可以遍历这个已选择的键集合来访问就绪的通道,如下:

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();
}

这个循环遍历已选择键集中的每个键,并检测各个键所对应的通道的就绪事件

注意每次迭代末尾的 keyIterator.remove() 调用,Selector 不会自己从已选择键集中移除 SelectionKey 实例,必须在处理完通道时自己移除,下次该通道变成就绪时,Selector 会再次将其放入已选择键集中,

SelectionKey.channel() 方法返回的通道需要转型成你要处理的类型,如 ServerSocketChannel 或 SocketChannel 等

6.5.2 wakeUp()

某个线程调用 select() 方法后阻塞了,即使没有通道已经就绪,也有办法让其从 select() 方法返回,只要让其它线程在第一个线程调用 select() 方法的那个对象上调用 Selector.wakeup() 方法即可,阻塞在 select() 方法上的线程会立马返回

如果有其它线程调用了 wakeup() 方法,但当前没有线程阻塞在 select() 方法上,下个调用 select() 方法的线程会立即“醒来(wake up)

6.5.3 close()

用完 Selector 后调用其 close() 方法会关闭该 Selector,且使注册到该 Selector 上的所有 SelectionKey 实例无效,通道本身并不会关闭

7. FileChannel

FileChannel 是连接到文件的 Channel,使用 FileChannel 可以从文件中读取数据,并将数据写入文件

FileChannel 不能设置为非阻塞模式,它始终以阻塞模式运行

7.1 打开 FileChannel

使用 FileChannel 前必须先打开它,但是我们不能直接打开 FileChannel,需要通过 InputStream、OutputStream 或 RandomAccessFile 获取 FileChannel,以下是通过 RandomAccessFile 打开 FileChannel 的方法:

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

7.2 从 FileChannel 读取数据

要想从 FileChannel 中读取数据,请调用其中一种 read() 方法,下面是一个例子:

ByteBuffer buf = ByteBuffer.allocate(48); 

int bytesRead = inChannel.read(buf);

首先分配一个 Buffer,从 FileChannel 中读取的数据被读入到 Buffer 中,

然后调用 FileChannel.read() 方法,此方法将数据从 FileChannel 读取到 Buffer, read() 方法返回的 int 值代表多少字节写入到 Buffer,如果返回 -1,则表示到达文件末尾

7.3 向 FileChannel 写数据

将数据写入到 FileChannel 是使用 FileChannel.write() 方法完成的,该方法将 Buffer 作为参数,下面是一个例子:

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

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

while (buf.hasRemaining()) {
    channel.write(buf);
}

请注意 FileChannel.write() 方法是在 while 循环中调用的,因为无法保证 write() 方法一次能向 FileChannel 写入多少字节,因此需要重复调用 write() 方法,直到 Buffer 没有更多字节要写入

7.4 关闭 FileChannel

当使用完 FileChannel 后,你必须关闭它,这是如何完成的:

channel.close(); 

7.5 FileChannel 的 position()

有可能需要在 FileChannel 的特定位置读取或写入,你可以通过 position() 方法获取对象的当前位置,

还可以通过调用 position(long pos) 方法来设置 position,

这里有两个例子:

long pos = channel.position();

channel.position(pos +123);

如果将 position 设置在文件结束符之后 ,并尝试从 Channel 中读取数据,读方法将返回 -1 - 文件结束标记

如果将 position 设置在文件结束符之后,然后向 Channel 中写数据,文件将被扩展以适应 position 和写入的数据,这可能导致“文件漏洞”,即磁盘上的物理文件在写入的数据中存在间隙

7.6 FileChannel 的 size()

FileChannel.size() 方法返回 Channel 连接到的文件的大小,这是一个简单的例子:

long fileSize = channel.size();

7.7 FileChannel 的 truncate()

通过调用 FileChannel.truncate() 方法来截断文件,截断一个文件时,文件会把指定长度后面的部分给删除,下面是一个例子:

channel.truncate(1024);

这个例子截取文件的前 1024 个字节,

7.7 FileChannel 的 force()

FileChannel.force() 方法将 Channel 中所有未写入的数据刷新到磁盘,操作系统可能出于性能原因将数据缓存在内存中,因此不能保证写入 Channel 的数据一定会写到磁盘上,直到调用 force() 方法,

该 force() 方法将一个布尔值作为参数,告知是否也将文件元数据(权限等)刷新到磁盘上,

这是一个同时刷新文件数据和元数据的示例:

channel.force(true);

8. SocketChannel

SocketChannel 是连接到 TCP 网络套接字的通道,它相当于 Java Networking 的 Sockets,有两种方法可以创建 SocketChannel:

  • 打开一个 SocketChannel 并连接到 Internet 上某处的服务器,
  • 一个新连接到达 SocketChannel 时,会创建一个 ServerSocketChannel

8.1 打开 SocketChannel

这是一个打开 SocketChannel 的方法:

SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));

8.2 关闭 SocketChannel

可以通过调用 SocketChannel.close() 方法来关闭 SocketChannel,这是如何完成的:

socketChannel.close();

8.3 从 SocketChannel 读取数据

要从 SocketChannel 中读取数据,请调用其中一种 read() 方法,下面是一个例子:

ByteBuffer buf = ByteBuffer.allocate(48); 

int bytesRead = socketChannel.read(buf);

首先分配 Buffer,从 SocketChannel 中读取的数据被读入到 Buffer 中,

然后调用 SocketChannel.read() 方法,此方法将数据从 SocketChannel 读到 Buffer 中, 由 read() 方法返回的 int 值表示读了多少字节到 Buffer, 如果返回 -1,则表示到达流末尾(连接关闭)

8.4 写入 SocketChannel

将数据写入到 SocketChannel 是使用 SocketChannel.write() 方法完成的,该方法将一个 Buffer 作为参数,下面是一个例子:

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

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

while (buf.hasRemaining()) {
    channel.write(buf);
}

请注意 SocketChannel.write() 方法是在 while 循环中调用的,因为无法保证 write() 方法能写多少字节到 SocketChannel,因此我们重复调用 write() 直到 Buffer 没有更多字节要写入

8.5 非阻塞模式

可以将 SocketChannel 设置为非阻塞模式,设置了之后,就可以在异步模式下调用 connect(),read() 和 write()

8.5.1 connect()

如果 SocketChannel 是在非阻塞模式下,此时调用 connect(),该方法可能在连接建立之前就返回了,为了确定连接是否建立,可以调用 finishConnect() 的方法,像这样:

socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));

while (!socketChannel.finishConnect()) {
    //wait, or do something else...    
}
8.5.2 write()

非阻塞模式下,write() 方法可能在尚未写出任何内容时就已经返回了,所以需要在循环中调用 write(),前面已经有例子了,这里就不赘述了

8.5.3 read()

非阻塞模式下,read() 方法可能在尚未读取到任何数据时就已经返回了,所以需要关注它的 int 返回值,它会告诉你读取了多少字节

8.6 非阻塞模式与 Selector

非阻塞模式与 Selector 一起工作得更好,通过将一个或多个 SocketChannel 注册到 Selector ,可以询问 Selector 哪个 Channel 已经准备好读取、写入等,如何使用 Selector 和 SocketChannel 将在本教程后面的文章中进行更详细的说明

9. ServerSocketChannel

ServerSocketChannel 是一个可以监听传入 TCP 连接的通道,就像标准 IO 中的 ServerSocket 一样,该 ServerSocketChannel 位于 java.nio.channels 包

下面是一个例子:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));

while (true) {
    SocketChannel socketChannel = serverSocketChannel.accept();
    // do something with socketChannel...
}

9.1 打开一个 ServerSocketChannel

通过调用 ServerSocketChannel.open() 方法来打开 ServerSocketChannel,这是一个示例:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

9.2 关闭一个 ServerSocketChannel

关闭 ServerSocketChannel 是通过调用 ServerSocketChannel.close() 方法完成的,这是一个示例:

serverSocketChannel.close();

9.3 监听新进来的连接

通过 ServerSocketChannel.accept() 方法监听新进来的连接,当 accept() 方法返回的时候,它返回一个包含新进来的连接的 SocketChannel,因此,accept() 方法会一直阻塞到有新连接到达

通常不会仅仅只监听一个连接,在 while 循环中调用 accept() 方法,如下面的例子:

while (true) {
    SocketChannel socketChannel = serverSocketChannel.accept();
    // do something with socketChannel...
}

当然,也可以在 while 循环中使用除了 true 以外的其它退出准则,

9.4 非阻塞模式

ServerSocketChannel 可以设置成非阻塞模式,在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是 null, 因此,需要检查返回的 SocketChannel 是否是 null,如:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);

while (true) {
    SocketChannel socketChannel = serverSocketChannel.accept();

    if ( socketChannel != null ) {
        // do something with socketChannel...
    }
}

10. 非阻塞服务器

11. DatagramChannel

DatagramChannel 是一个可以发送和接收 UDP 数据包的通道,由于 UDP 是一种无连接的网络协议,所以不能像其他通道那样读取和写入,相反,它发送和接收的是数据包

11.1 打开 DatagramChannel

这是打开一个 DatagramChannel 的方法:

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

本示例打开的 DatagramChannel 可以在 UDP 的 9999 端口上接收数据包

11.2 接收数据

通过调用 receive() 方法从 DatagramChannel 接收数据,如下所示:

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();

channel.receive(buf);

receive() 方法将接收到的数据包的内容复制到给定的 Buffer,如果接收到的数据包包含的数据多于 Buffer 可以包含的数据,则剩余的数据将被静默丢弃

11.3 发送数据

通过调用 DatagramChannel 的 send() 方法来发送数据,如下所示:

String newData = "New String to write to file..." + System.currentTimeMillis();
    
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();

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

本示例将字符串发送到“jenkov.com”服务器的 UDP 80 端口上,因为没有在该端口上监听任何内容,所以不会发生任何事情,也不会通知你发到的数据包是否收到,因为 UDP 不保证数据的传输

12.4 连接到特定的地址

可以将 DatagramChannel “连接”到网络中的特定地址的,由于 UDP 是无连接的,连接到特定地址并不会像 TCP 通道那样创建一个真正的连接,而是锁住 DatagramChannel,让其只能从特定地址收发数据,

这里有个例子:

channel.connect(new InetSocketAddress("jenkov.com", 80));    

当连接后,也可以使用 read() 和 write() 方法,就像在用传统的通道一样,只是在数据传送方面没有任何保证,这里有几个例子:

int bytesRead = channel.read(buf);
int bytesWritten = channel.write(buf)

13. Pipe

Java NIO 管道是 2 个线程之间的单向数据连接,Pipe 有一个 source 通道和一个 sink 通道,数据会被写到 sink 通道,从 source 通道读取,

下面是Pipe原理图:

image.png

13.1 创建管道

通过 Pipe.open() 方法打开管道,例如:

Pipe pipe = Pipe.open();

13.2 向管道写数据

要向管道写数据,需要访问 sink 通道,像这样:

Pipe.SinkChannel sinkChannel = pipe.sink();

通过调用 SinkChannel 的 write() 方法,将数据写入 SinkChannel,像这样:

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

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

while (buf.hasRemaining()) {
    sinkChannel.write(buf);
}

13.3 从管道读取数据

从读取管道的数据,需要访问 source 通道,像这样:

Pipe.SourceChannel sourceChannel = pipe.source();

调用 source 通道的 read() 方法来读取数据,像这样:

ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf);

read() 方法返回的 int 值会告诉我们多少字节被读进了缓冲区

14. NIO 和 IO

15. Path

16. File

17. AsynchronousFileChannel

Q.E.D.


盛年不重来,一日难再晨。