Java NIO学习(一)

Java NIO是一套可供选择的IO API,旨在替代标准的Java IO和Java Networking API。Java NIO提供了与标准IO不同的处理方式。

Java NIO引入

1. Channels 和 Buffers(通道和缓冲区)

在标准的IO API中通常使用字符流或字节流的方式来处理数据,而在NIO中是则是使用通道(channel)和缓冲区(buffer)。数据会被从一个channel读取到一个buffer,或者从一个buffer写入到一个channel。

2. Non-blocking IO

Java NIO实现了异步非阻塞的IO。例如,一个线程可以申请一个channel来读取数据到一个buffer中。当线程从channel读取数据到buffer中时,该线程还可以做其它的事情。当数据被写入到buffer后,然后该线程可以继续对该数据进行处理。从缓冲区写入通道也类似。

3. Selectors(选择器)

Java NIO引入了selectors的概念。selector是用于监听多个channel事件(连接打开,数据到达)的对象。 因此,单个线程可以监听多个channel获取数据。


Java NIO概述

Java NIO 包括Channels、Buffers和Selectors三个核心组件。除此之外,Java NIO的类和组件还有很多,但是这三个组件构成了API的核心部分。其它的组件,如Pipe和FileLock,仅仅是通用的工具类,用来组合这三个核心组件。因此,我们的重点是这三个核心组件。

1. Channels和Buffers

基本上所有的 IO操作 在NIO 中都是从一个Channel 开始。Channel有点像流,数据可以从Channel读到Buffer中。同样,数据可以从Buffer写到Channel。
数据读写
这里有几个Channel和Buffer的类型。下面是几个Java NIO中主要的Channel实现:

FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel

这些channels涵盖了UDP和TCP网络IO和文件IO。
下面是几个NIO中的重要Buffer实现:

ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer

这些Buffer涵盖了通过标准IO进行传递的基本数据类型:byte、short、int、long、float、double和char。
Java NIO也有一个MappedByteBuffer用来表示内存映射文件,后文中会介绍到。

2. Selectors

Selector允许单线程处理多个 Channel。如果你的应用程序打开了多个连接(Channel),但每个连接上的流量都很低。
selector模型
例如,在一个聊天服务器中。要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法将会处于阻塞状态,直到某个已经注册的channel有事件就绪。一旦方法返回,该线程就可以处理这些事件。事件的实例如:传入新的连接,数据接收等。


Java NIO 通道

Java NIO Channels和流很相似,但是有一些不同的地方:

既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。 
通道可以异步地读写。 
通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。

从通道读取数据到缓冲区,从缓冲区写入数据到通道。

1. Channel的实现

这里列出了在Java NIO中几个最重要的通道实现:

FileChannel:从文件中读取数据;
DatagramChannel:可以通过UDP协议读写网络数据;
SocketChannel:可以通过TCP协议读写网络数据;
ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每个新进来的连接都会创建一个SocketChannel。

2. 一个通道示例

使用FileChannel读取数据到一个Buffer中。

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
    FileChannel inChannel = aFile.getChannel();//创建文件通道

    ByteBuffer buf = ByteBuffer.allocate(48);//创建一个容量为48bytes的缓冲区

    int bytesRead = inChannel.read(buf);//将数据从通道读入到缓冲区
    while (bytesRead != -1) {

      System.out.println("Read " + bytesRead);
      buf.flip();//将buffer切换成读模式

      while(buf.hasRemaining()){
          System.out.print((char) buf.get());//从缓冲区获取数据
      }

      buf.clear();//将buffer切换成写模式
      bytesRead = inChannel.read(buf);
    }
    aFile.close();

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


Java NIO缓冲区

Java NIO的Buffers通常用于和NIO Channels进行交互。众所周知,数据是从channels读入到buffers中,从buffers写出到channels中的。
缓冲区本质上是一块可以写数据,然后从中可以读取数据的内存。这块内存被包装成一个NIO Buffer对象,提供了一组方法使得方位该内存块变得很方便。

1. 缓冲区的基本用法

使用一个Buffer来读写数据一般遵循以下四个步骤:

写入数据到Buffer中;
调用buffer.flip()方法;
从Buffer读取数据;
调用buffer.clear()或buffer.compact()。

当向一个buffer中写数据时,buffer会记录下写了多少数据。当要读取数据时,需要调用flip()方法将buffer从写模式切换到读模式。在读模式中,b可以读取之前写到buffer中的所有数据。
当读完了所有的数据,需要清空这个buffer,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只是清空已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

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. 缓冲区的Capacity,Position和Limit

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。
为了解Buffer的工作原理,需要熟悉的Buffer三个属性包括:

capacity
position
limit

position和limit是依赖于这个Buffer是处在读模式还是写模式。不管Buffer处在什么模式,capacity的含义总是一样的。
缓冲区三属性

3. Capacity

作为一个内存块,Buffer有一个固定的大小值,或者叫做capacity(容量)。你只能往里写capacity个bytes,long,chars等类型的数据到buffer中。一旦buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。

4. Position

当你往Buffer中写数据时,position表示当前的位置。初始的position值为0。当一个byte,long等数据写入到Buffer后,position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1。
当从一个buffer读取数据时,也是从某个特定的位置读取。当你flip一个buffer从写模式到读模式,position被重置为0。当从缓冲区的position处读取数据时,position向前移动到下一个可读的位置。

5. Limit

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

6. Buffer类型

ByteBuffer
MappedByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer

这些buffer类型代表了不同数据类型。换句话说,就是可以通过char,short,int,long,float 或 double类型来操作缓冲区中的字节。
MappedByteBuffer 有些特别,在涉及它的专门章节中再讲。

7. 缓冲区的分配

要想获得一个Buffer对象,首先要进行分配。每个Buffer类都有一个allocate()方法。

ByteBuffer buf = ByteBuffer.allocate(48);

CharBuffer buf = CharBuffer.allocate(1024);

8. 向缓冲区写数据

从Channel写到buffer。
通过Buffer的put()方法写到Buffer里。
int bytesRead = inChannel.read(buf); //read into buffer.

buf.put(127);   

put方法有很多版本,允许你以不同的方式把数据写入到Buffer中。例如, 写到一个指定的位置,或者把一个字节数组写入到Buffer。

9. flip()

flip()方法将一个buffer从写模式切换到读模式。调用flip()方法会将position设置为0,并且设置limit为之前的position位置。

10. 从缓冲区读取数据

可以通过两种方式从Buffer中读取数据:

从buffer中读取数据到一个channel
使用get()方法从Buffer中读取数据
//read from buffer into channel.
int bytesWritten = inChannel.write(buf);

byte aByte = buf.get();    

get方法有很多版本,允许你以不同的方式从Buffer中读取数据。例如,从指定position读取,或者从Buffer中读取数据到字节数组。

11. Rewind

Buffer.rewind()设置position位置为0,所以你可以重新读取buffer中所有的数据。limit保持不变,仍然表示能从buffer中读取多少个元素。

12. clear() and compact()

一旦读完Buffer中的数据,需要让buffer准备好再次被写入。可以通过调用clear()或compact()方法。
如果调用clear方法,position会被设置为0,limit会被设置为capacity。换句换说,Buffer被清空了。在Buffer中的数据没有被清除。只是这些标记告诉我们可以从可以从哪里开始往buffer中写数据。
如果在buffer中还有一些未读的数据,调用clear方法将会使得这些数据被遗忘,意味着不再有任何标记哪些数据被对过,哪些还没有。
如果Buffer中仍有未读数据,而且后续还需要这些数据,但是此时想要先写一些数据,那么使用compact()方法。
compact方法会将所有未读的数据拷贝到Buffer起始处,然后将position设置为最后一个未读元素的正后面,limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。

13. mark() and 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.  

14. equals() and compareTo()

可以使用equals()和compareTo()方法比较两个Buffer。
(1)equals()
当满足下列条件时,表示两个Buffer相等:

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

equals只是比较buffer的一部分,不是每一个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素。
(2) comparTo()
比较两个buffer中剩余的元素(bytes,chars等)例如排序。如果

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

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

Java NIO Scatter / Gather(分散/聚集)

Java NIO内置分散/聚集支持。分散/聚集是用于描述从通道张读取或写入到通道的操作。
分散:从channel读取是指在操作时将读取的数据写入多个buffer中。因此,channel将从channel中读取的数据分散到多buffer中。
聚集:写入channel是指在写操作时,将多个buffer的数据写入到同一个channel,因此,channel将多个buffer中的数据聚集后发送到channel。
scatter / gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样你可以方便的处理消息头和消息体。

1. Scattering Reads

数据从一个channel读取到多个buffer中。
分散

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

ByteBuffer[] bufferArray = { header, body };

channel.read(bufferArray);

注意,buffer首先被插入到一个数组,然后再将数组作为channel.read()的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。
事实上在移动到下一个buffer之前,必须填满当前buffer,意味着它不适用于动态信息(消息大小不固定)。换句话说,如果存在消息头和消息体,消息头必须完成填充分散读才能正常工作。

2. Gathering Writes

聚集写是指数据从多个buffer写入到一个channel。
聚集

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

//write data into buffers

ByteBuffer[] bufferArray = { header, body };

channel.write(bufferArray);

buffer数组作为write方法的输入参数,顺序地将buffer中的数据写入到channel。
注意,只有在position和limit之间的数据才会被写入。因此,如果一个buffer有128字节,但是只包含了58字节的数据,那么这58字节的数据将被写入到channel中。因此聚集写操作可以很好的处理动态信息,和分散读正好相反。

Java NIO 通道之间的数据传输

在JavaNIO中,你可以直接从一个channel向另一个channel传输数据,如果其中一个channel是FileChannel,则这个FileChannel类由一个transferTo和一个transferFrom方法。

1. transferFrom

FileChannel.transferFrom()方法可以将数据从源通道传输到FileChannel中。(将字节从给定的可读取字节通道传输到此通道的文件中)

RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel      fromChannel = fromFile.getChannel();

RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel      toChannel = toFile.getChannel();

long position = 0;
long count    = fromChannel.size();

toChannel.transferFrom(fromChannel, position, count);

方法的输入参数position表示从position处开始向目标文件写入数据,count表示最多传输的字节数。如果源通道的剩余空间小于 count 个字节,则所传输的字节数要小于请求的字节数。
此外要注意,在SoketChannel的实现中,SocketChannel只会传输此刻准备好的数据(可能不足count字节)。因此,SocketChannel可能不会将请求的所有数据(count个字节)全部传输到FileChannel中。

2. transferTo

该方法是将数据从FileChannel传输到其它的channel中。

RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel      fromChannel = fromFile.getChannel();

RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel      toChannel = toFile.getChannel();

long position = 0;
long count    = fromChannel.size();

fromChannel.transferTo(position, count, toChannel);

注意这个和前面的非常相似,除了调用方法的FileChannel对象不一样外,其他的都一样。
上面所说的关于SocketChannel的问题在transferTo()方法中同样存在。SocketChannel会一直传输数据直到目标buffer被填满。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值