概述:
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);