首先了解一下NIO中的重要概念 通道、缓冲区、选择器
通道- 类似于流,但是可以异步读写数据(流只能同步读写),通道是双向的,(流是单向的),通道的数据总是要先读到一个buffer 或者 从一个buffer写入,即通道与buffer进行数据交互。
通道类型:
o FileChannel:从文件中读写数据的通道。通过使用一个InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例,FileChannel比较特殊,它可以与通道进行数据交互, 不能切换到非阻塞模式,套接字通道可以切换到非阻塞模式。
o DatagramChannel:能通过UDP读写网络中的数据的通道,直接通过DatagramChannel.open()获取一个DatagramChannel实例。
o SocketChannel:能通过TCP读写网络中的数据的通道,直接通过SocketChannel.open()获取一个SocketChannel实例。
o ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel,直接通过ServerSocketChannel.open()获取一个ServerSocketChannel实例。
缓冲区- 本质上是一块可以存储数据的内存,被封装成了buffer对象!
缓冲区类型:
o ByteBuffer
o MappedByteBuffer
o CharBuffer
o DoubleBuffer
o FloatBuffer
o IntBuffer
o LongBuffer
o ShortBuffer
缓冲区常用方法:
o allocate() - 分配一块缓冲区,可以指定缓冲区大小
o put() - 向缓冲区写数据
o get() - 向缓冲区读数据
o array() - 返回缓冲区内容的字节数组
o filp() - 翻转缓冲区,读写模式进行翻转
o clear() - 从写模式切换到读模式,不会清空数据,如果想循环将缓冲区中的数据提取出来进行输出,不进行clear()和filp()的话,将会出现死循环,如果是在一个循环内filp()放置在clear()的后面,也会出现死循环,使用时根据实际情况进行设置。
o compact() - 从读数据切换到写模式,数据不会被清空,会将所有未读的数据copy到缓冲区头部,后续写数据不会覆盖,而是在这些数据之后写数据
o mark() - 对position做出标记,配合reset使用
o reset() - 将position置为标记值
缓冲区的一些属性:
o capacity - 缓冲区大小,无论是读模式还是写模式,为激活时设定大小,此属性值不会变;
o position -
写数据时,position表示当前写的位置,每写一个数据,会向下移动一个数据单元,初始为0;
最大为capacity - 1;切换到读模式时,position会被置为0,表示当前读的位置从哪开始
o limit - 写模式下,limit 相当于capacity 表示最多可以向缓冲区写多少数据,切换到读模式时,limit 等于原先的position,表示最多可以从缓冲区读多少数据。
下面是一个File的读写示例:case.txt中内容为this is test
public static void readFileByNIO(){
File f = new File("C:\\Users\\zht\\Desktop\\case.txt");
try {
FileInputStream in = new FileInputStream(f);
//从文件字节流中获取一个文件通道
FileChannel channel = in.getChannel();
//构建一个缓冲区,并指定容量为100个字节
ByteBuffer buffer = ByteBuffer.allocate(100);
System.out.println("写入缓冲区前限制数:" + buffer.limit() + " 容量是:" + buffer.capacity()
+ " 位置为:" + buffer.position());
int bytes = -1;
//从通道中写入缓冲区,为写模式,buffer的position值变为从通道中读取的字节数
while((bytes = channel.read(buffer)) != -1){
System.out.println("字节数:"+bytes);
System.out.println("写入缓冲区后限制数:" + buffer.limit() + " 容量是:" + buffer.capacity()
+ " 位置为:" + buffer.position());
//将字节转换编码进行输出
System.out.println("读取文件内容:"+new String(buffer.array(),0,bytes,"utf-8"));
}
in.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
输出结果:
写入缓冲区前限制数:100 容量是:100 位置为:0
字节数:15
写入缓冲区后限制数:100 容量是:100 位置为:15
读取文件内容:this is test
从以上这些结果中,看似正常,实际在应用中,我们的文件中的内容不可能只有100个字节以内,所以一旦超出100个字节,上面示例将会出现死循环,下面就针对这个问题进行修改:
public static void readFileByNIO(){
File f = new File("C:\\Users\\zht\\Desktop\\case.txt");
try {
FileInputStream in = new FileInputStream(f);
//从文件字节流中获取一个文件通道
FileChannel channel = in.getChannel();
//构建一个缓冲区,并指定容量为100个字节
ByteBuffer buffer = ByteBuffer.allocate(10);
StringBuilder builder = new StringBuilder();
System.out.println("写入缓冲区前限制数:" + buffer.limit() + " 容量是:" + buffer.capacity()
+ " 位置为:" + buffer.position());
int bytes = -1;
//从通道中写入缓冲区,为写模式,buffer的position值变为从通道中读取的字节数
while((bytes = channel.read(buffer)) != -1){
System.out.println("字节数:"+bytes);
System.out.println("写入缓冲区后限制数:" + buffer.limit() + " 容量是:" + buffer.capacity()
+ " 位置为:" + buffer.position());
//使用clear获或者是flip
buffer.clear();//这里是将缓冲区从写模式转换为读模式,因为下面要将数据从缓冲区中将数据拿出来打印
// buffer.flip();//这里是对缓冲区进行翻转,如果这里两个方法一起用也将导致死循环
System.out.println("翻转缓冲区后限制数:" + buffer.limit() + " 容量是:" + buffer.capacity()
+ " 位置为:" + buffer.position());
//将字节转换编码进行输出
builder.append(new String(buffer.array(),0,bytes,"utf-8"));
}
System.out.println("读取文件内容:"+builder.toString());
in.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
输出结果:
写入缓冲区前限制数:10 容量是:10 位置为:0
字节数:10
写入缓冲区后限制数:10 容量是:10 位置为:10
翻转缓冲区后限制数:10 容量是:10 位置为:0
字节数:5
写入缓冲区后限制数:10 容量是:10 位置为:5
翻转缓冲区后限制数:10 容量是:10 位置为:0
读取文件内容:this is test
以上修改后的代码,我将缓冲区容量改小了,文件中内容没变,从结果可以看出从通道写入缓冲区两次,写入缓冲区后pos位置是不一样的,是根据字节数的变化而变化,filp后或者是clear后,pos位置是都回到了初始化的位置。你可以认为是在filp或者clear后,你不指定起止位置的情况下,从缓冲区提取数据都是从0位置开始取到limit位置,包含空闲的缓冲区空间,而我这里是因为设置了起止值new String(buffer.array(),0,bytes,"utf-8")。下面我们再来验证一下clear或者filp后,原缓冲区的数据是否还存在?
public static void readFileByNIO(){
File f = new File("C:\\Users\\zht\\Desktop\\case.txt");
try {
FileInputStream in = new FileInputStream(f);
//从文件字节流中获取一个文件通道
FileChannel channel = in.getChannel();
//构建一个缓冲区,并指定容量为100个字节
ByteBuffer buffer = ByteBuffer.allocate(10);
System.out.println("写入缓冲区前限制数:" + buffer.limit() + " 容量是:" + buffer.capacity()
+ " 位置为:" + buffer.position());
int bytes = -1;
//从通道中写入缓冲区,为写模式,buffer的position值变为从通道中读取的字节数
while((bytes = channel.read(buffer)) != -1){
System.out.println("字节数:"+bytes);
System.out.println("写入缓冲区后限制数:" + buffer.limit() + " 容量是:" + buffer.capacity()
+ " 位置为:" + buffer.position());
//使用clear获或者是flip
buffer.clear();//这里是将缓冲区从写模式转换为读模式,因为下面要将数据从缓冲区中将数据拿出来打印
// buffer.flip();//这里是对缓冲区进行翻转
System.out.println("翻转缓冲区后限制数:" + buffer.limit() + " 容量是:" + buffer.capacity()
+ " 位置为:" + buffer.position());
//将字节转换编码进行输出
System.out.println("读取文件内容:"+new String(buffer.array(),"utf-8"));
}
in.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
输出结果:
写入缓冲区前限制数:10 容量是:10 位置为:0
字节数:10
写入缓冲区后限制数:10 容量是:10 位置为:10
翻转缓冲区后限制数:10 容量是:10 位置为:0
读取文件内容:this is
字节数:5
写入缓冲区后限制数:10 容量是:10 位置为:5
翻转缓冲区后限制数:10 容量是:10 位置为:0
读取文件内容: testis is //这里很明显的内容不对
一个从A文件到B文件的NIO读写例子:case.txt中内容不变
public static void writFileByNIO(){
File f = new File("C:\\Users\\zht\\Desktop\\case.txt");
File f2= new File("C:\\Users\\zht\\Desktop\\case2.txt");
try {
FileInputStream in = new FileInputStream(f);
//从文件字节流中获取一个文件通道
FileChannel channel = in.getChannel();
//构建一个缓冲区,并指定容量为100个字节
ByteBuffer buffer = ByteBuffer.allocate(10);
FileOutputStream out = new FileOutputStream(f2);
FileChannel channel2 =out.getChannel();
System.out.println("缓冲区输入前限制是:" + buffer.limit() + "容量是:" + buffer.capacity()
+ "位置是:" + buffer.position());
int bytes = -1;
while((bytes = channel.read(buffer)) != -1){
System.out.println("缓冲区输入后限制是:" + buffer.limit() + "容量是:" + buffer.capacity()
+ "位置是:" + buffer.position());
ByteBuffer buffer2 = Charset.forName("utf-8").encode(new String(buffer.array(),0,bytes,"utf-8"));
while(buffer2.hasRemaining()){
channel2.write(buffer2);
System.out.println("缓冲区输出后限制是:" + buffer.limit() + "容量是:" + buffer.capacity()
+ "位置是:" + buffer.position());
}
buffer.flip();
System.out.println("缓冲区翻转后限制是:" + buffer.limit() + "容量是:" + buffer.capacity()
+ "位置是:" + buffer.position());
}
out.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
选择器:相当于一个观察者,用来监听通道感兴趣的事件,一个选择器可以绑定多个通道。
通道向选择器注册时,需要指定感兴趣的事件,选择器支持以下事件:
o SelectionKey.OP_CONNECT
o SelectionKey.OP_ACCEPT
o SelectionKey.OP_READ
o SelectionKey.OP_WRITE
如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来,如下:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
通道向选择器注册时,会返回一个 SelectionKey对象,具有如下属性
o interest集合
o ready集合
o Channel
o Selector
o 附加的对象(可选)
用“位与”操作interest 集合和给定的SelectionKey常量,可以确定某个确定的事件是否在interest 集合中。
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
ready 集合是通道已经准备就绪的操作的集合。在一次选择(Selection)之后,你会首先访问这个ready set。Selection将在下一小节进行解释。
可以这样访问ready集合:
int readySet = selectionKey.readyOps();
可以使用以下四个方法获取已就绪事件,返回值为boolean:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
可以将一个对象或者更多信息附着到SelectionKey上,即记录在附加对象上,方法如下:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
可以通过选择器的select方法获取是否有就绪的通道,返回值表示上次执行select之后,就绪通道的个数。
int select();
int select(long timeout);
int selectNow();
可以通过selectedKeySet获取已就绪的通道。返回值是SelectionKey 的集合,处理完相应的通道之后,需要removed 因为Selector不会自己removed
select阻塞后,可以用wakeup唤醒;执行wakeup时,如果没有阻塞的select那么执行完wakeup后下一个执行select就会立即返回。
调用close() 方法关闭selector