写在前面
前段时间工作之余了解了一下 NIO ,现在拿出来做以总结,打算花6篇左右的博客,对 NIO 的基本特性和日常使用,做个小小的总结。
NIO简介
有过一定JAVA基础的朋友们应该都对 IO (输入输出)有一定的了解,那么 NIO 其实是JDK1.4之后推出的一种新的 IO 类型,当时因为对高并发和大数据处理的要求并不高,并且 NIO 与传统 IO 的原理和使用方式都完全不同,所以并没有普及,之后经过 JDK 几个版本的改良,并且市场环境发生变化之后,NIO 才真正的大规模投入了使用。
对于 NIO 这个名字其实并不难理解,官方的解释是傻瓜式的,即 New IO 就是新的 IO,也可以理解为这是官方在命名上的词穷和偷懒的表现。NIO 和 IO 有着同样的作用和目的,但是除了这一点共性,两者在大多数地方都有着不同之处。
NIO 和 IO 的主要区别
首先传统的 IO 是面向流的,这个流我理解为水流,要传输的数据就是水,大江东去浪淘尽,水的流向总是向着大海,没听说过海里的水还能倒流的,即使要倒流那也得人工搞一个水泵才行。所以,传统的 IO流 是单向的,流分为两种:输入流和输出流,各自分别负责输入和输出。
而 NIO 则是面向缓冲区的,在 NIO 中有两个概念:通道和缓冲区,通道我理解为铁路,是连接两地(数据源和程序)的一个媒介,它并没有传输的功能,铁路要进行运输光有铁路可不行,还得需要火车,而这个火车就是缓冲区,所以缓冲区是依赖通道的。当然,铁路肯定有往返票,不是我从广州去了躺西安我就回不来了,所以 NIO 是双向的。
上面用到了很多比喻,我也不能保证这个比喻绝对的正确,但是能说明问题是关键,下面的这个图说明了 NIO 和 IO 的主要区别。
其中阻塞和非阻塞这两个概念是网络编程中用的比较多,选择器(Selectors)是 NIO 通过一个线程去监控多个通道的工具,这些都会在之后讨论。
Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开到 IO 设备(例如:文件、套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。简而言之,Channel负责传输,Buffer负责存储。
缓冲区的基本属性和使用
缓冲区(Buffer)是一个用于特定的基本数据类型的容器,用来存放数据,在 NIO 中所有类型缓冲区都是 Buffer 抽象类的子类,用缓冲区可以对数据进行读写操作。
根据数据类型的不同,缓冲区有以下类型:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
可以看到除了boolean类型之外,缓冲区涵盖了JAVA的所有基本数据类型。
对于缓冲区,有以下几个重要的概念:
- 容量 (capacity) :表示 Buffer 最大数据容量,缓冲区容量不能为负,并且创建后不能更改。
- 限制 (limit):第一个不应该读取或写入的数据的索引,即位于 limit 后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量。
- 位置 (position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制
- 标记 (mark)与重置 (reset):标记是一个索引,通过 Buffer 中的 mark() 方法指定 Buffer 中一个特定的 position,之后可以通过调用 reset() 方法恢复到这个 position
Buffer 的常用方法如下图所示:
另外,读写数据分别为get()和put()方法,参数类型有很多,就不一一解释,都是很常用的参数类型。
接下来,我用一段代码来解释 Buffer 常用的操作和各个属性的意义:这段代码从分配一个缓冲区开始,对一个缓冲区做简单的写操作和读操作,并且每一个操作之后都对当前缓冲区的状态进行查看,来观察缓冲区属性发生的变化。
代码中有详细的注释,大家可以运行一下试试,都是很基础并且必要的操作,对理解缓冲区有很大帮助。
//该方法输出当前缓冲区的状态
public void viewStatus(Buffer buf){
System.out.println("当前正在操作数据的位置:"+buf.position());
System.out.println("缓冲区中可操作数据的大小:"+buf.limit());
System.out.println("缓冲区中最大存储数据的容量:"+buf.capacity());
}
@Test
public void test1(){
String str = "madrid";
//1.分配一个指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
System.out.println("--------------allocate()--------------");
this.viewStatus(buf);
//2.利用put方法向缓冲区中存储数据
buf.put(str.getBytes());
System.out.println("--------------put()--------------");
this.viewStatus(buf);
//3.利用flip方法切换为读取数据的模式
buf.flip();
System.out.println("--------------flip()--------------");
this.viewStatus(buf);
//4.利用get()方法读取缓冲区中的数据
byte[] dst = new byte[buf.limit()];
buf.get(dst);
System.out.println(new String(dst,0,dst.length));
System.out.println("--------------get()--------------");
this.viewStatus(buf);
//5.rewind():可再次读取数据
buf.rewind();
System.out.println("--------------rewind()--------------");
this.viewStatus(buf);
//6.clear():清空缓冲区,但是缓冲区中的数据依然存在,只不过这些数据处于“被遗忘状态”
buf.clear();
System.out.println("--------------clear()--------------");
this.viewStatus(buf);
}
运行结果如下图
对于这段代码,每个操作之后缓冲区的状态的改变一定要理解,可以把缓冲区看做一个数组来理解。
另外还有三个方法,mark()和reset()以及remaining()分别用于标记当前操作的位置和恢复到标记的位置以及获取缓冲区中可以操作数据的数量,这些是很常用的方法,关于这三个方法的简单使用如下:
public void test2(){
String str = "madrid";
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put(str.getBytes());
buf.flip();
byte[] dst = new byte[buf.limit()];
buf.get(dst,0,2);
System.out.println(new String(dst,0,2));
this.viewStatus(buf);
//mark():标记
buf.mark();
buf.get(dst,2,2);
System.out.println(new String(dst,2,2));
this.viewStatus(buf);
//reset():恢复到标记的位置
buf.reset();
System.out.println("------------reset----------------");
this.viewStatus(buf);
//判断缓冲区中是否还有数据
if(buf.hasRemaining()){
//获取缓冲区中可以操作的数量
System.out.println("缓冲区中可以操作的数量:"+buf.remaining());
}
}
运行结果如下图:
至此对于 NIO 的缓冲区(Buffer)的基本属性和方法,以及 NIO 和 IO 的区别做了一个小小的总结,对于缓冲区(Buffer)其实还有直接缓冲区和非直接缓冲区两个概念,这些将在下篇博文介绍。
笔者水平有限,若有错漏,欢迎指正,如果转载以及CV操作,请务必注明出处,谢谢!
版权声明:本文为博主原创文章,未经博主允许不得转载。