Java NIO (New IO) 是一个替代接口(从Java 1.4开始),相对于Java传统IO接口和网络接口而言。NIO处理IO的方式和传统IO会有很大区别。标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。注意:传统IO是单向。
IO | NIO |
面向流 | 面向缓冲区 |
阻塞IO | 非阻塞IO |
无 | 选择器 |
Java IO和NIO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
传统的IO通信:磁盘→系统→程序→socket→协议引擎
NIO:磁盘→系统→socket→协议引擎
通道表示打开到 IO 设备(例如:文件、套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。 Channel相比IO中的Stream更加高效,可以异步双向传输,但是必须和buffer一起使用。
主要实现类
FileChannel,读写文件中的数据。
SocketChannel,通过TCP读写网络中的数据。
ServerSockectChannel,监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
DatagramChannel,通过UDP读写网络中的数据。
主要获取方式
1、java针对支持通道的类提供了getChannel()方法
本地io:
FileInputStreanm/FileOutputStream
RandomAccessFile
网络io:
Socket
ServerSocket
DatagramSocket
FileInputStream fis = new FileInputStream("D:\\1.jpg");
FileChannel inChannel = fis.getChannel();
2、 jdk1.7的nio2只对各个通道提供了一个静态方法open()
FileChannel inChannel = FileChannel.open(Paths.get("D:\\1.jpg"), StandardOpenOption.READ);
通道之间的数据传输
1、read&write
//将 Buffer 中数据写入 Channel
outChannel.write(buff)
//从 Channel 读取数据到 Buffer
inChannel.read(buff)
2、transferFrom
从源信道读取字节到这个通道的文件中。如果源通道的剩余空间小于 count 个字节,则所传输的字节数要小于请求的字节数。这种方法可能比从源通道读取并写入此通道的简单循环更有效率。
@param SRC 源通道
@param position 调动开始的文件内的位置,必须是非负的
@param count 要传输的最大字节数,必须是非负
@return 传输文件的大小(单位字节),可能为零,
public abstract long transferFrom(ReadableByteChannel src, long position, long count) throws IOException;
//复制图片,利用直接缓存区
public void test() throws Exception{
FileChannel inChannel = FileChannel.open(Paths.get("D:\\1.jpg"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("D:\\2.jpg"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
outChannel.transferFrom(inChannel,0, inChannel.size());
inChannel.close();
outChannel.close();
}
3、transferTo
将字节从这个通道的文件传输到给定的可写字节通道。
@param position 调动开始的文件内的位置,必须是非负的
@param count 要传输的最大字节数,必须是非负
@param target 目标通道
@return 传输文件的大小(单位字节),可能为零,
public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;
//复制图片,利用直接缓存区
public void test2() throws Exception{
FileChannel inChannel = FileChannel.open(Paths.get("D:\\1.jpg"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("D:\\3.jpg"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
inChannel.transferTo(0, inChannel.size(), outChannel);
inChannel.close();
outChannel.close();
}
Buffer
是一个对象, 它包含一些要写入或者刚读出的数据。 在 NIO 中加入 Buffer
对象,体现了新库与原 I/O 的一个重要区别。在面向流的 I/O 中,您将数据直接写入或者将数据直接读到 Stream
对象中。在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任何时候访问 NIO 中的数据,您都是将它放到缓冲区中。缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。但是一个缓冲区不 仅仅 是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
这就是Buffer的大致继承体系,很多都没有罗列出来,因为主要讲解的是ByteBuffer.这里大概说一下,Buffer是一个抽象类,包括一个Buffer的最基本属性,比如,容量,位置,上界,标记.子类通过调用父类构造方法来实例化这几个参数,子类也都有各自的容量实现.比如ByteBuffer类用字节数组当缓冲区.旗下又有两个自己的实现类.
在jdk中,buffer有很多中实现,例如intBuffer,LongBuffer,ByteBuffer等,但是在NIO中实现的大多使用的是ByteBuffer,重点来看一下ByteBuffer.
Buffer的属性
容量(capacity):缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变
上界(limit):缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数
位置(position):下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新
标记(mark):下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新一个备忘位置。调用 mark( )来设定 mark = postion。调用 reset( )设定 position =mark。标记在设定前是未定义的(undefined)。这四个属性之间总是遵循以下关系:0 <= mark <= position <= limit <= capacity
我们先看一个Demo,根据demo在细说
@Test
public void test006(){
try{
//1.新建缓冲区
ByteBuffer byteBuffer =ByteBuffer.allocate(10);
System.out.println("1.新建缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
//2.第一次存放HELLO 5个字节到缓冲区
String str1 = "HELLO";
byteBuffer.put(str1.getBytes());
System.out.println("2.第一次输入HELLO,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
//3.第二次存放JM 2个字节到缓冲区
String str2 = "JM";
byteBuffer.put(str2.getBytes());
System.out.println("3.第二次输入JM,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
//4.开启读的模式
byteBuffer.flip();
System.out.println("4.开启读模式之后,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
//5.从缓冲区取4个字节,并标记
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes,0,4);//获取缓冲区数据
byteBuffer.mark();//mark是一个索引,通过此方法指定Buffer中一个特定的position
System.out.println("5.从缓冲区取4个字节写入到通道,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
//6.将缓冲区剩余的字节取出
byteBuffer.get(bytes,byteBuffer.position(),byteBuffer.limit()-byteBuffer.position());
System.out.println("6.将缓冲区剩余的字节取出写入到通道,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
//7.调用reset()方法恢复到mark标记的position位置
byteBuffer.reset();
System.out.println("7.调用reset()方法恢复到mark标记的位置,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
//8.重设缓冲区以便接收更多的字节
byteBuffer.clear();
System.out.println("8.调用缓冲区的clear(),缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
}catch(Exception e){
e.printStackTrace();
}
}
输出结果:
1.新建缓冲区【position:0 Limit:10 Capacity:10】
2.第一次输入HELLO,缓冲区【position:5 Limit:10 Capacity:10】
3.第二次输入JM,缓冲区【position:7 Limit:10 Capacity:10】
4.开启读模式之后,缓冲区【position:0 Limit:7 Capacity:10】
5.从缓冲区取4个字节写入到通道,缓冲区【position:4 Limit:7 Capacity:10】
6.将缓冲区剩余的字节取出写入到通道,缓冲区【position:7 Limit:7 Capacity:10】
7.调用reset()方法恢复到mark标记的位置,缓冲区【position:4 Limit:7 Capacity:10】
8.调用缓冲区的clear(),缓冲区【position:0 Limit:10 Capacity:10】
我们首先新创建的缓冲区。出于本例子的需要,我们假设这个缓冲区的 总容量
为10个字节。 Buffer
的状态如下所示:
//1.新建缓冲区
ByteBuffer byteBuffer =ByteBuffer.allocate(10);
System.out.println("1.新建缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
1.新建缓冲区【position:0 Limit:10 Capacity:10】
回想一下 ,limit
决不能大于 capacity
,此例中这两个值都被设置为 10。我们通过将它们指向数组的尾部之后(如果有第10个槽,则是第10个槽所在的位置)来说明这点。
position
设置为0。如果我们读一些数据到缓冲区中,那么下一个读取的数据就进入 slot 0 。如果我们从缓冲区写一些数据,从缓冲区读取的下一个字节就来自 slot 0
现在我们可以开始在新创建的缓冲区上进行读/写操作。首先从输入通道中读一些数据到缓冲区中。第一次读取得到五个字节。它们被放到数组中从 position
开始的位置,这时 position 被设置为 0。读完之后,position 就增加到 5,如下所示:
//2.第一次存放HELLO 5个字节到缓冲区
String str1 = "HELLO";
byteBuffer.put(str1.getBytes());
System.out.println("2.第一次输入HELLO,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
2.第一次输入HELLO,缓冲区【position:5 Limit:10 Capacity:10】
我们可以看出Limit和Capacity没有改变,Position的值从0变为5。
在第二次读取时,我们从输入通道读取另外两个字节到缓冲区中。这两个字节储存在由 position
所指定的位置上, position
因而增加 2:
//3.第二次存放JM 2个字节到缓冲区
String str2 = "JM";
byteBuffer.put(str2.getBytes());
System.out.println("3.第二次输入JM,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
3.第二次输入JM,缓冲区【position:7 Limit:10 Capacity:10】
我们可以看出Limit和Capacity没有改变,Position的值从5变为7。
现在我们要将数据写到输出通道中。在这之前,我们必须调用 flip()
方法。这个方法做两件非常重要的事:
- 它将
limit
设置为当前position
。- 它将
position
设置为 0。
下面是在 flip 之后的缓冲区:
//4.开启读的模式
byteBuffer.flip();
System.out.println("4.开启读模式之后,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
4.开启读模式之后,缓冲区【position:0 Limit:7 Capacity:10】
我们现在可以将数据从缓冲区写入通道了。 position
被设置为 0,这意味着我们得到的下一个字节是第一个字节。 limit
已被设置为原来的 position
,这意味着它包括以前读到的所有字节,并且一个字节也不多。
在第一次写入时,我们从缓冲区中取四个字节并将它们写入输出通道。这使得 position
增加到 4,而 limit
不变,如下所示
//5.从缓冲区取4个字节,并标记
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes,0,4);//获取缓冲区数据
byteBuffer.mark();//mark是一个索引,通过此方法指定Buffer中一个特定的position
System.out.println("5.从缓冲区取4个字节写入到通道,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
5.从缓冲区取4个字节写入到通道,缓冲区【position:4 Limit:7 Capacity:10】
我们只剩下一个字节可写了。 limit
在我们调用 flip()
时被设置为 5,并且 position
不能超过 limit
。所以最后一次写入操作从缓冲区取出一个字节并将它写入输出通道。这使得 position
增加到 5,并保持 limit
不变,如下所示:
//6.将缓冲区剩余的字节取出
byteBuffer.get(bytes,byteBuffer.position(),byteBuffer.limit()-byteBuffer.position());
System.out.println("6.将缓冲区剩余的字节取出写入到通道,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
6.将缓冲区剩余的字节取出写入到通道,缓冲区【position:7 Limit:7 Capacity:10】
//7.调用reset()方法恢复到mark标记的position位置
byteBuffer.reset();
System.out.println("7.调用reset()方法恢复到mark标记的位置,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
7.调用reset()方法恢复到mark标记的位置,缓冲区【position:4 Limit:7 Capacity:10】
最后一步是调用缓冲区的 clear()
方法。这个方法重设缓冲区以便接收更多的字节。 Clear
做两种非常重要的事情:
- 它将
limit
设置为与capacity
相同。- 它设置
position
为 0。
下图显示了在调用 clear()
后缓冲区的状态:
//8.重设缓冲区以便接收更多的字节
byteBuffer.clear();
System.out.println("8.调用缓冲区的clear(),缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
8.调用缓冲区的clear(),缓冲区【position:0 Limit:10 Capacity:10】
缓冲区现在可以接收新的数据了。