概述:
NIO包是JDK1.4引入的新的I/O类库,目的是为了提高文件读写的速度。NIO的读写模式和旧的I/O有一些不同,NIO是通过缓冲器和通道来对文件进行操作的,当我们需要对文件进行数据的读取时,我们先将数据读取到缓冲器中,再从缓冲器上读取我们所要的数据,当我们想往文件中写入数据时,那么我们先朝着缓冲器写入数据,再利用缓冲器往文件里进行写入。在整个文件的读写操作时,我们并没有与文件进行直接的交互。
我们先来看一个NIO读写文件的例子
public static void testChannel() throws IOException {
//1.利用RandomAccessFile打开文件data.txt
RandomAccessFile aFile = new RandomAccessFile(dir + "data.txt","rw");
//2.获取文件的通道
FileChannel inChannel = aFile.getChannel();
//3.创建一个字节缓存器
ByteBuffer buf = ByteBuffer.allocate(48);
//4.根据缓存器的大小将文件内容读取到缓存中
int bytesRead = inChannel.read(buf);
//5.根据读取到的字节数做判断
while (bytesRead != -1){
System.out.println("Read " + bytesRead);
//切换缓冲区的模式
buf.flip();
//6.打印缓存中的内容
while (buf.hasRemaining()){
System.out.print((char) buf.get());
}
System.out.println("");
//7.清空缓存的内容,假如不执行这一步,read(buf)将会返回0,因为无法读入到buf中
buf.clear();
bytesRead = inChannel.read(buf);
}
aFile.close();
}
如上所示,这是一个利用Channel和缓冲器从文件中读取数据的代码,上面的代码分为这几个步骤:
- 打开一个文件
- 获取文件的Channel
- 创建一个固定大小的缓冲器
- 将文件内容读入到缓冲器中
- 读出缓冲器的内容,读完之后清空缓冲器以便接下来继续读
NIO的组成
如下所示,一个NIO的系统的组成大致包含下面的三大部分,分别是通道(Channel)、缓冲器(Buffers)以及Selector

其中Selector的作用是可以在单个线程中管理多个通道的读写
Buffers(缓冲器)
缓冲器是文件数据读取和写入的一段内存,当我们需要从通道中取数据时,我么先将通道中的数据读到缓冲器中,再从缓冲器中获取数据。当我们需要往通道中写入数据时,我们先将数据写到缓冲器中,再把数据写入通道。
缓冲器的三个标识
capacity、limit和position,下面我们来分别介绍一下
capacity
表示缓存的容量,比如说我们使用下面的代码创建了一个缓冲器
ByteBuffer buf = ByteBuffer.allocate(48);//分配48个Byte的缓冲器
那么缓冲器的capacity就是48,注意capacity的值与缓冲器的类型无关
limit
- 在读模式下,limit表示缓冲器还有多少数据可以读
- 在写模式下,limit表示缓冲器还有多少空间可以往里写
position
表示缓冲区的当前位置
- 在缓冲区切到写模式时,position的值被设为0
- 在缓冲区切换到读模式时,position的值被设为缓冲区中第一个能写入的空间位置
buffer一些操作的方法
往buffer里面写入数据
- 通过Channel往buffer里写入数据,如下所示
int byteRead = inChannel.read(buf);
上面是往缓存buffer里面写入数据,并返回写入到缓存中的字节个数
- 通过buffer的put()方法往缓存中写入数据
buf.put(127);
往缓存中写入ASCII编码为127的数,写入之后,limit减1,position加1
flip()方法
这个方法是将缓存切换到读模式以读取缓存中的数据,具体的变化是将position的值设为0,limit的值设为原先position的值,在这样的情况下,就可以读取buffer中从position到limit之间的所有数据
从buffer中读取数据
- 从buffer中读取数据到Channel
int bytesWritten = inChannel.write(buf);
上面是将buffer中的数据读取到Channel中,并返回读取数据的个数
- 使用
get()方法读取数据,如下所示
byte aByte = buf.get();
获取buffer中当前位置的值,并将结果赋值给aByte
rewind()方法
这个方法的作用是重新读取buffer里面的内容,做法是,将position置为0,而limit保持不变
clear()和compact()方法
clear()方法是对buffer进行清空操作,但是并不是真正意义上的清空,而是修改其中的标识的值,将position的值设为0,而将limit的值设为capacity,这样再往buffer里面写入数据时就会覆盖原先的内容,已达到清空的目的,但是如果随后没有执行写操作,那么原来的数据还是能读的出来
compact()则是将buffer中所有未读的数据拷贝到buffer的起始位置,再将position的值设置到最后一个未读元素的后面
mark()和reset()方法
mark()方法将buffer中的position的值记录下来(赋值给mark),假如我们继续往后面读取数据,这时候我们调用reset()方法时,我们会回到我们之前标记的位置,实际上是将mark的值回赋给position
equals()和compareTo()方法
这两个方法都是对两个buffer之间的比较
对于equals(),满足下面的几个条件时,我们会返回true值,表示两个buffer相同
1. 两个buffer有相同的类型
2. Buffer中剩余的byte、char等的个数相等
3. Buffer中所有剩余的byte、char等都相同
从上面的条件我们可以看出,equals()方法实际上是比较从position到limit之间的数据是否相等,而对position之前的数据则并不关心
compareTo()方法在满足下列所有条件时,表示一个Buffer小于另一个Buffer
1. 第一个不相等的元素小于另一个Buffer中对应的元素
2. 所有元素都相等,但第一个Buffer比另一个先耗尽
compareTo()方法返回的是第一个buffer和第二个buffer中第一个不相等值的差
scatter和gather
scatter
scatter是指在读操作时将一个Channel中的数据读出到多个buffer中去,代码如下所示
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);
如上的代码是一个将一个Channel中的数据读出到多个buffer中的例子,在读出的过程中,会沿着buffer数组的下标依次进行填满,这样的做法不适合于动态的信息
gather
gather指在写操作时将多个buffer的数据写入到同一个Channel,代码如下所示
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);
如上的代码是将不同的buffer沿着buffer的下标依次写入到Channel中去,这样的操作很适合处理动态的信息
Channel之间的数据传输
利用Channel的transferFrom()和transferTo()方法可以在不借助额外的buffer而完成两个Channel之间的数据传输
transferFrom()
将任何Channel中的数据传输到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(position, count, fromChannel);
transferTo()
将FileChannel中的数据传输到任意的Channel中去,示例代码如下
RandomAccessFile fromFile = new RandomAccessFile(dir + "data.txt","rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile(dir + "data2.txt","rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
fromChannel.transferTo(position,count,toChannel);
Selector
Selector的作用是在单一的线程下对多个Channel中的数据进行管理,这样就很有利于对于单个Channel数据量少,但是Channel的总数多的情形进行管理
创建一个Selector
Selector selector = Selector.open();
向Selector注册通道
channel.configureBlocking(false);//将Channel设为非阻塞
SelectionKey key = channel.register(selector,SelectionKey.OP_READ);//将Channel注册到Selector对象上去
注册在Selector中的Channel必须处于非阻塞模式,这意味着不能将Selector和FileChannel一起使用,因为FileChannel是阻塞通道
interest集合
Channel的register方法的第二个参数是interest的集合,意味着第二个参数是多个类型的叠加,类型包括:
Connect(OP_CONNECT)
Accept(OP_ACCEPT)
Read(OP_READ)
Write(OP_WRITE)
第二个参数不仅可以是上面4个单独的类型,而且可以是上面4个类型的叠加,即使用按位或的方式,例如: SelectionKey.OP_READ | SelectionKey.OP_ACCEPT,这表示对这个Channel的连接和读取感兴趣
SelectionKey
当Channel向Selector注册时,会返回一个SelectionKey的对象,该对象包含以下的属性
interest集合(int interestSet = selectionKey.readOps();)
ready集合(int readySet = selectionKey.readyOps();)
Channel(Channel channel = selectionKey.channel();)
Selector(Selector selector = selectionKey.selector();)
附加的对象
select()
这个方法表示对注册在Selector上的Channel进行选择,select()的方法有几种变体,如下
int select():阻塞并一直等到通道上有一个Channel中发生了其在注册时指定的事件,比如定义了OP_READ属性且出现了Channel的读取行为
int select(long timeout):阻塞并且等到设置的时间后自动返回
int selectNow():立即返回,不管有没有选择到正在发生注册操作的Channel
selectedKeys()
在使用select()方法后会知道有一个或多个通道已经处于就绪状态了,那么此时可以使用selectedKeys()来获取SelectionKey对象的集合,如下所示
Set selectedKeys = selector.selectedKeys();
对这些结果集的操作如下列代码所示
Set selectedKeys = selector.selectedKeys();
Iterator 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不会自己移除SelectionKey实例,因此需要利用这个方法来进行移除操作
}
wakeUp()
如果一个线程正在执行select()并且因此而处于阻塞的状态,那么可以让其他线程在第一个处于调用select()而阻塞的线程上调用Selector.WakeUp()而立即返回
close()
用完Selector后可以利用close()方法来关闭Selector,这样会使得注册在其上的Channel无效,但是这并不能关闭Channel
FileChannel
FileChannel是文件操作的通道,其操作的方法见下面
打开FileChannel
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
从FileChannel读取数据
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
向FileChannel中写入数据
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之后关闭Channel
channel.close();
position方法
position方法有两种形式,带参数和不带参数,例子如下
long pos = channel.position();//无参形式为获取当前通道的当前位置
channel.position(pos + 123);//带参形式为设置通道的当前位置
需注意的是,在设置通道当前位置的时候,假如设置的位置超出了文件长度,并且会在设置的该位置处写下数据,那么就会造成文件的空洞
size()
获取通道关联的文件的大小
long fileSize = channel.size();
truncate(int)
从头截取通道关联文件的大小,并且丢弃掉后面的数据
channel.truncate(1024);//只需要关联文件的头1024个字节
force()
将通道中尚未写入到磁盘里的数据写入到磁盘里
channel.force(true);
SocketChannel
SocketChannel是一个连接到TCP套接字上的通道,创建一个SocketChannel的方式有下面两种
1. 打开一个SocketChannel并连接到互联网上的某台服务器。
2. 一个新连接到达ServerSocketChannel时,会创建一个SocketChannel
打开SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://www.baidu.com",80));
关闭SocketChannel
socketChannel.close();
从SocketChannel中读取数据
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);
写入SocketChannel
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.configureBlocking(false);//将SocketChannel对象设为非阻塞
socketChannel.connect(new InetSocketAddress("http://jenkov.com",80));
while(! socketChannel.finishConnect()){
}
write()
在非阻塞模式下,write()方法在尚未写出任何内容时就可能返回了,所以在循环中调用write()
read()
非阻塞模式下,read()方法在尚未读取到任何数据时就可能返回了,所以要关注其返回的字节数
DatagramChannel
DatagramChannel的打开方式
DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9999));
打开一个DatagramChannel,并将这个Channel绑定到UDP的9999端口
接收数据
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);//利用分配的buffer进行接收数据,超出buffer大小的数据将被丢弃
发送数据
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("fanwn.com", 80));
以上一个发送数据的代码,由于其发送的地址并没有处于监听状态,因此这样不会发生任何反应
连接到特定的地址
channel.connect(new InetSocketAddress("fanwn.com",80));
int bytesRead = channel.read(buf);
int bytesWritten = channel.write(buf);
将Channel连接到特定的地址,可以读取通道中的数据到buffer中,也可以将buffer中的数据读取到通道中
Pipe
管道是对两个线程之间交换数据的方式,一个管道包含了两个通道类:sink通道和source通道,其中sink是往通道里面写入数据,source是从通道内读出数据
打开管道
Pipe pipe = Pipe.open();
向管道写入数据
Pipe.SinkChannel sinkChannel = pipe.sink();
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);
}
从管道中读出数据
Pipe.SourceChannel sourceChannel = pipe.source();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = sourceChannel.read(buf);