欢迎访问:我的个人网站
NIO
NIO(Non Blocking IO / New IO)于JDK1.4引入,可用于代替标准的JavaIO API, NIO与原来的IO有着相同的作用于目的,但是二者在使用上以及特点上还是有较大区别。NIO相较于传统的IO能进行更加高效的读写操作。NIO与传统IO的区别主要体现在以下几个地方:
- 传统IO是面向流(Stream)的, 但是NIO是面向缓冲区的(Buffer Oriented)。
- 传统IO是阻塞的,但是NIO是非阻塞的。
- NIO在使用上多了个选择器(Selectors)的概念
- 传统IO是单向的(例:文件输入流,仅表示一个输入的过程),但是NIO的数据传送可以是双向(打开一个通道,以读写的模式打开)的。
在使用NIO的时候,涉及到两个概念:通道, 缓存区 , 它们可以视作为NIO的核心。通道表示一个从应用程序打开到IO设备(文件,网络套接字)的连接。而缓冲区则视作为在通道上装载数据的区域,我们可以操作位于通道上的缓冲区,进行对数据进行处理。形象化理解:通道可以理解为是连接两地(应用程序&IO设备)的铁路。而缓存区是位于这个铁路上的火车。火车可以装载货物在两地往返(数据传送&双向)。简而言之,通道负责进行传输工作,而缓存区负责进行数据的临时存储。下面就这两个概念进行进一步的展开:
1.缓存区
在Java NIO中负责进行数据的存取,缓存区的底层还是使用数据进行实现的,以存储不同类似的数据进行传递,根据我们使用的数据类型,JDK提供了与之对应的类进行装载,示例(部分):
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
1.1相关属性
以上部分实现类均为抽象类Buffer的子类,Buffer类为其子类实现了一组通用的操作,使得各个具体类在使用上具有较强的通用性。就缓存区而言,它具有几个需要注意的属性:
- capacity: 容量
- limit: 可操作范围索引
- position: 操作位置索引
- mark: 标记索引
以上几个属性满足关系:0<=mark<=position<=limit<=capacity
capacity
前面说过,缓存区实际上还是使用数组结构作为其底层实现的,那么既然是数组,就必须要又一个固定的大小,而决定这个固定数组大小的值,就是capacity。这个值是在我们申请缓存区空间的时候进行指定的,在指定之后就无法再次进行改变,后续所要容纳的数据量也不能操作该大小,否则将导致java.nio.BufferOverflowException
limit
针对一个缓冲区而言,该数据指示了一个缓冲区可以被操作的区域大小 , 换句话说,limit指定了缓存区的一个索引位置,在该索引位置之后的数据是禁止读写操作的。**当我们进行写操作的时候,该值与capacity的大小一致,因为写操作模式下,我们最多可以操作capacity大小的空间,limit也就理所当然的与其一致了。但是在写操作模式下,该值等于我们已经存储的数据长度。 ** 该值在切换模式时自动转换无需我们操心。
position
该数据是在对缓存区数据进行存取操作时,用于指示操作的索引位置, 具体如下:当写模式下,该值等于下一个即将被存储数据的索引位置,此时索引指向的位置是不包含数据的。在读模式下,该值等于准备被读取数据的索引位置,此时指向的位置是存在数据的。
mark
可以标记一个position的值到mark中,后续我们可以再使用相关方法恢复position的值而无需关心在此期间position做了何种变化。简单地说,mark在某个时间记录了position的值,在之后的某个时间可以将position恢复到之前标记的位置。
1.2相关方法
下面将就结合上面介绍的属性,说明针对缓存区操作的一些方法:
分配缓存区空间
ByteBuffer allocate(int capacity):
使用该方法可以建立一个指定大小的缓存区,该方法为其具体缓存区类型中的静态方法,所分配的空间仍属于JVM管理的内存范围
//源码, 在堆上建立一个缓存区
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw createCapacityException(capacity);
return new HeapByteBuffer(capacity, capacity);
}
ByteBuffer allocateDirect(int capacity):
该方法也是用于分配缓存区空间,但是该方法是在直接内存 区域进行划分的,直接内存区域不属于JVM管理,在JVM内存模型之外。介于此特点可知,使用这个方法进行缓存区划分的话,将不再受制于堆大小的限制,但是仍然会受制于物理主机的实际内存大小以及分页,最大寻址空间等的限制。
无论使用何种方式进行申请,在后续的使用上几乎是没区别的,在申请一个缓存区之后,默认的各项属性如下:
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
System.out.println("capacity :" +byteBuffer.capacity());
System.out.println("limit : "+byteBuffer.limit());
System.out.println("mark : "+byteBuffer.mark());
System.out.println("position :" + byteBuffer.position());
//输出:
capacity :10
limit : 10
mark : java.nio.HeapByteBuffer[pos=0 lim=10 cap=10]
position :0
存数据:put()
将数据存入一个缓存区中,该方法提供了多种的重载形式,可以直接传入另一个缓存区对象,或者一个与之对应类型的数组,如下当我们存入数据之后,缓存区属性发生变化:
FloatBuffer floatBuffer = FloatBuffer.allocate(10);
byteBuffer.put("bestbigkk".getBytes());
System.out.println("position :" +byteBuffer.position());
System.out.println("limit :"+byteBuffer.limit());
//输出:
position :9
limit :10
读写切换:flip()
更改对一个缓存区的操作模式,更改实质上是对缓存区相关属性的调整:
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
//写模式
byteBuffer.put("bestbigkk".getBytes());
System.out.println("position :" +byteBuffer.position());
System.out.println("limit :"+byteBuffer.limit());
//切换为读模式
byteBuffer.flip();
System.out.println("position :" +byteBuffer.position());
System.out.println("limit :"+byteBuffer.limit());
//输出
position :9
limit :10
position :0
limit :9
读取数据 get()
在正确切换读写模式的前提下,对数据进行读取操作, 方法仍旧提供了几种重载的形式,根据需要进行选择:
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
//写模式
byteBuffer.put("bestbigkk".getBytes());
//切换为读模式
byteBuffer.flip();
System.out.println("position :" +byteBuffer.position());
System.out.println("limit :"+byteBuffer.limit());
//读取
byte[] buff = new byte[byteBuffer.limit()];
byteBuffer.get(buff);
System.out.println(new String(buff));
System.out.println("position :" +byteBuffer.position());
System.out.println("limit :"+byteBuffer.limit());
//输出
position :0
limit :9
bestbigkk
position :9
limit :9
复位读取位置 rewind()
调用该方法将导致position重新指向数据的开始位置,以便进行再次的读取操作
清空缓存区 clear()
调用该方法以清空缓存区中的内容,需要注意的是该方法仅仅将缓存区的几个属性重新置为默认状态,数组中存在的数据实际上并没有被清空,只是允许后续对该数据进行覆盖操作:
//源码
public Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
标记mark() / 复位 reset()
标记一个position的位置,并在后续使用reset()进行恢复,如果当前缓存区已经被清空,则无法进行标记操作.
获取可读取数据大小
可读取数据,也就是position与limit的差。如果positin小于limit则认为有数据还未读取
1.3 直接缓存区,非直接缓存区
在建立缓存区的时候,可以选择在JVM管理的堆上建立,此时的缓存区成为非直接缓存区,数据的交互仍旧要通过中间缓存空间进行,存在复制行为。同样的,我们也可以在直接内存建立缓存区,此时该区域将不再由JVM进行管理,因此使用直接缓存区对应用程序内存空间造成的影响很小。
使用直接直接缓存区可以通过FileChannel的map()方法将一个文件映射到内存中进行处理,该方法会返回一个MappedByteBuffer对象(内存映射文件),我们在这个区域操作对象就可以视为直接操作了磁盘的文件。
如果要在直接缓存区中创建一个内存映射文件,只有ByteBuffer支持。
2.通道
可以视为程序与设备(本地,网络套接字)所建立起的一个连接,与传统IO的流相似,通道与Buffer进行数据交互,因为其本身是不存取数据的,该工作由缓存区承担。通道存在几个主要的实现类,以上除FileChannel外,其余的通道均应用于网络IO环境下:
- FileChannel
- SocketChannel
- ServerSocketChannel
- DatagramChannel
2.1 获取通道
(1)JDK允许从支持通道的类中获取,使用getChannel()方法即可,包含:FileInputStream,FileOutputStream,RandomAccessFile,Socket, ServerSocket,DatagramSocket。
FileInputStream fis = new FileInputStream("d:\\waterPic.png");
FileOutputStream fos = new FileOutputStream("d:\\1.png");
//获取通道
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel();
//分配缓存区
ByteBuffer buff = ByteBuffer.allocate(1024);
//将通道的数据存入缓存区
while (inChannel.read(buff) != -1) {
buff.flip();
outChannel.write(buff);
buff.clear();
}
inChannel.close();
outChannel.close();
fos.close();
fis.close();
(2)JDK1.7之后,针对1中的各个类提供了一个静态方法open(Path path, OpenOption… options)来获取 。
path表示建立通道的文件路径,可以使用Paths类进行生成,可变参数options表示对这个文件进行的操作方式,StandardOpenOption类下定义了关于此操作的一些常量。CREATE常量表示:不存在就创建,存在就覆盖,与之对应的还有一个CREATE_NEW,表示如果文件存在就报错,如果不存在就创建文件。
需要注意的是,在指定映射模式MapModel的时候,必须被它通道本身的操作所支持,例如:针对outChannel,它支持读操作,写操作,新建操作。那么针对这个通道建立缓存区的时候,MapMode指定了READ_WRITE, 表示要进行读写操作,这两种操作在outChannel中是被支持的。反之则会抛出java.nio.channels.NonReadableChannelException (取消了outChannel对读操作的支持)
FileChannel inChannel = FileChannel.open(Paths.get("d:\\waterPic.png"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("d:\\1.png"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE_NEW);
//建立缓存区,与之前的allocate()获取到的内存空间是相同的,同样表示把一个文件的数据加载到内存空间中
MappedByteBuffer inMappedByteBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMappedByteBuffer = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());
//将缓存区的数据传递到另一个通道的缓存区中
byte[] dst = new byte[inMappedByteBuffer.limit()];
inMappedByteBuffer.get(dst);
outMappedByteBuffer.put(dst);
inChannel.close();
outChannel.close();
(3)JDK1.7之后,使用工具类Files中的newByteChannel()获取通道。其余操作类似,这里不再赘述
FileChannel inChannel = Files.newByteChannel(Paths.get("d:\\waterPic.png"), StandardOpenOption.CREATE);
以上三种方式均可以进行文件的传输工作,通过指定一个较大的缓存区,我们可以在进行文件传输工作的时候,明显的观察到内存占用情况的改变,这里演示拷贝一个1.5GB的视频由E盘至D盘的情况,可以明显观察到:伴随着磁盘0的使用率上升,内存使用量也在增加,在全部读取到内存之后,开始进行复制工作,此时磁盘2使用率开始上升:
另外在两个通道转移数据的时候,可以使用更加较为便捷的方法:transferTo(int position, long count, WritableByteChannel target),transferFrom(ReadableByteChannel src, long position, long count)
这两个方法内部实际上与上面的原理类似,不过是对外而言隐藏了这些细节。
FileChannel inChannel = FileChannel.open(Paths.get("e:\\Videos\\猩球崛起3.mp4"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("d:\\1.mp4"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE_NEW);
//方式1.将一个通道内的数据转移到另一个通道中,其内部还是使用了缓存区,简化了流程
inChannel.transferTo(0, inChannel.size(), outChannel);
//方式2.获取一个通道内的数据到本通道
outChannel.transferFrom(inChannel, 0, inChannel.size());
inChannel.close();
outChannel.close();
3.分散(Scatter)与聚集(Gather)
简单而言,分散就是将一个通道所指向文件的数据存在多个缓存区中,这个分散的过程是按照缓存区的顺序进行的。聚集与之相反,将多个缓存区内的数据按照顺序存到一个通道中。这里仅仅使用一些代码演示:
3.1 分散
//文件内容: www.bestbigkk.com
FileChannel inChannel = new RandomAccessFile("d:\\text.txt","rw").getChannel();
ByteBuffer[] buffers = {ByteBuffer.allocate(2), ByteBuffer.allocate(2), ByteBuffer.allocate(2)};
inChannel.read(buffers);
buffers[2].flip();
System.out.println(new String(buffers[2].array(), StandardCharsets.UTF_8));
inChannel.close();
//输出:be
3.2 聚集
//约定此时buffers中已经按照3.1的代码读入了数据
for (var b : buffers) {
b.flip();
}
FileChannel outChannel = new RandomAccessFile("d:\\copy.txt", "rw").getChannel();
outChannel.write(buffers);
outChannel.close();
inChannel.close();
//copy.txt文件中的内容:www.be
4.编码解码
在使用其他Buffer类型的时候,有时候会遇到乱码的问题,此时可以使用相关操作更改解码编码方式:
CharBuffer buffer = CharBuffer.allocate(32);
buffer.put("最棒的大开开".toCharArray());
buffer.flip();
Charset charset = Charset.forName("UTF16");
CharsetDecoder decoder = charset.newDecoder();
CharsetEncoder encoder = charset.newEncoder();;
ByteBuffer byteBuffer = encoder.encode(buffer);// UTF16
CharBuffer charBuffer = decoder.decode(byteBuffer); // 还原为默认编码
System.out.println(new String(byteBuffer.array(), 0, byteBuffer.limit(), StandardCharsets.UTF_16)); //以UTF16解码
System.out.println(new String(charBuffer.array(), 0, charBuffer.limit()));// 以默认编码解码
5.选择器
选择器是实现NIO的一个核心组件,是SelectableChannel的多路复用器,用于监控SelectableChannel的IO状况,在它下面有以下集合子类:
- SocketChannel
- ServerSocketChannel
- DatagramChannel
- Pipe.SinkChannel
- Pipd.SourceChannel
我们可以调用以上类的register(Selector sel, int ops)方法将一个通道注册到指定的选择器上面,选择器可使用以下方式获取:
Selector selector = Selector.open();
在绑定一个通道到选择器上面的时候,还需要指定要进行监听的操作类型,该操作类型通过register的第二个参数进行指定,有以下几个选择:
- 读: SelectionKey.OP_READ = 1
- 写: SelectionKey.OP_WRITE = 4
- 连接: Selection.OP_CONNECT = 8
- 接收: Selection.OP_ACCEPT = 16
如果要同时进行多种操作类型的监听,可以使用或运算符进行实现:
int opType = SelectionKey.OP_READ | SelectionKey.OP_ACCEPT;
使用选择器之前,需要使用configureBlocking(false);方法配置当前通道为非阻塞的。
6.阻塞与非阻塞式IO
Java NIO使我们可以进行非阻塞IO操作。比如说,单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。而Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情。
6.1 阻塞式的IO示例
如果一个IO的传输过程是阻塞式的,那么操作会在一个线程上等待直至其处理完成,这里写了一个简单的例子来说明这个问题。客户端向服务器发送数据,发送完成之后服务器进行一次响应以确认上传结果。在这里如果客户端发送完数据之后没有调用shutdowmOutput() 方法。则服务器会一直在线程上等待数据的继续传递而不会进行响应操作。
@Test
public void client() throws Exception{
//建立本地文件通道
FileChannel fileChannel = FileChannel.open(Paths.get("D:\\waterPic.png"), StandardOpenOption.READ);
//获取远程连接通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 1314));
System.out.println("已连接到服务器...");
//获取本地文件内存映射
MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
//写入到远程连接通道
socketChannel.write(buffer);
//主动终止输出, 如果不加这句,则服务器会在线程上一直等待数据传递而不会进行相应
socketChannel.shutdownOutput();
System.out.println("数据发送完成,等待回应...");
//从远程连接通道读取服务器的回应
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
while (socketChannel.read(byteBuffer) != -1) {
byteBuffer.flip();
System.out.println(byteBuffer.mark());
System.out.println(new String(byteBuffer.array(), 0, byteBuffer.limit()));
byteBuffer.clear();
}
socketChannel.close();
fileChannel.close();
}
@Test
public void server() throws Exception{
//获取本地文件通道
FileChannel fileChannel = FileChannel.open(Paths.get("D:\\copy.png"), StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
//获取远程连接通道
ServerSocketChannel socketChannel = ServerSocketChannel.open();
socketChannel.bind(new InetSocketAddress(1314));
System.out.println("监听中..");
SocketChannel acceptChannel = socketChannel.accept();
System.out.println("客户端已连接...");
ByteBuffer buffer = ByteBuffer.allocate(512);
//从远程连接通道读取数据到本地文件通道
while (acceptChannel.read(buffer) != -1) {
buffer.flip();
fileChannel.write(buffer);
buffer.clear();
}
//回应
buffer.put("接收完成".getBytes());
buffer.flip();
acceptChannel.write(buffer);
acceptChannel.shutdownOutput();
fileChannel.close();
acceptChannel.close();
socketChannel.close();
}
6.2 非阻塞式IO
@Test
public void client() throws Exception{
//建立本地文件通道
FileChannel fileChannel = FileChannel.open(Paths.get("D:\\waterPic.png"), StandardOpenOption.READ);
MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
//获取远程连接通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 1314));
System.out.println("已连接到服务器...");
//切换为非阻塞模式
socketChannel.configureBlocking(false);
//传递数据到远程连接的通道中
socketChannel.write(buffer);
socketChannel.close();
fileChannel.close();
}
@Test
public void server() throws Exception{
//获取远程连接通道
ServerSocketChannel socketChannel = ServerSocketChannel.open();
System.out.println("监听中..");
socketChannel.bind(new InetSocketAddress(1314));
//切换为非阻塞模式
socketChannel.configureBlocking(false);
//获取选择器
Selector selector = Selector.open();
//将远程连接通道注册到选择器上, 监听该通道的连接状态, 并指定选择键为“接收“
socketChannel.register(selector, SelectionKey.OP_ACCEPT);
//轮询获取该选择器上面已经就绪的通道, 这里的“就绪”指的是存在通道满足了选择器的一个或多个监听类型(读,写,可连接,可接收)
while (selector.select() > 0) {
System.out.println("Selectable:"+selector.select());
//获取这个选择器中所有注册的选择键
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
//获取这个准备就绪的选择键
SelectionKey selectionKey = keyIterator.next();
//判断键类型,
//是“可接收类型”
if (selectionKey.isAcceptable()) {
SocketChannel acceptChannel = socketChannel.accept();
System.out.println("客户端已连接...");
acceptChannel.configureBlocking(false);
//将这个新建立的通道注册到选择器上面,并监听其“读”状态
acceptChannel.register(selector, SelectionKey.OP_READ);
}else if(selectionKey.isReadable()){//是“可读”状态
System.out.println("传递数据");
//获取这个可读取的通道
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(512);
//从远程连接通道读取数据到本地文件通道
int len = 0;
//获取本地文件通道
FileChannel fileChannel = FileChannel.open(Paths.get("D:\\"+UUID.randomUUID().toString()+".png"), StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
while ((len = channel.read(buffer)) > 0) {
buffer.flip();
fileChannel.write(buffer);
buffer.clear();
}
channel.close();
fileChannel.close();
}
keyIterator.remove();
}
}
socketChannel.close();
}
7. DatagramChannel
用于接收UDP包的一个通道,使用上与上面的其他通道基本类似,不同的是数据的发送以及接收对应的方法名为:send() / receive() 这里只做出一个简单的使用演示说明即可:
客户端多次发送数据到服务器,服务器接收并显示
@Test
public void client() throws Exception{
while (true) {
out.println("发送");
DatagramChannel channel = DatagramChannel.open();
channel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(128);
buffer.put((UUID.randomUUID().toString() +"\n").getBytes());
buffer.flip();
channel.send(buffer, new InetSocketAddress("127.0.0.1", 1314));
buffer.clear();
channel.close();
Thread.sleep(2000);
}
}
@Test
public void server() throws Exception{
DatagramChannel channel = DatagramChannel.open();
channel.configureBlocking(false);
channel.bind(new InetSocketAddress(1314));
Selector selector = Selector.open();
channel.register(selector, SelectionKey.OP_READ);
ByteBuffer buffer = ByteBuffer.allocate(128);
while (selector.select() > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isReadable()) {
DatagramChannel readChannel = (DatagramChannel) selectionKey.channel();
readChannel.receive(buffer);
buffer.flip();
out.println(new String(buffer.array(), 0, buffer.limit()));
}
}
iterator.remove();
}
channel.close();
}
8. Pipe
这种通道可以用于两个线程之间的通信,Pipe对象包含了两种通道类型:SinkChannel / SoureceChannel, 我们可以向SinkChannel通道写入信息,另一个线程从SourceChannel通道进行读取工作,以达到线程间通信的目的。 下面是对这个管道的简单使用:
public class Main {
Pipe pipe;
Pipe.SinkChannel sinkChannel;
Pipe.SourceChannel sourceChannel;
@Before
public void before() throws Exception{
pipe = Pipe.open();
sinkChannel = pipe.sink();
sourceChannel = pipe.source();
}
@Test
public void pipe() throws InterruptedException {
//sender
new Thread(()->{
String newData = "另一个线程, 你好!";
ByteBuffer buf = ByteBuffer.allocate(48);
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
try {
sinkChannel.write(buf);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
Thread.sleep(1000);
//recevier
new Thread(()->{
try {
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = sourceChannel.read(buf);
System.out.println(new String(buf.array(), 0, bytesRead));
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}