前言
上一篇文章认识了一下Java的三大IO,这一章节我们详细了解一下NIO的工作原理以及三大核心Selector,Channel,Buffer并尝试来做一些小案例。
Java NIO 模型
Java NIO有三个核心的组件: selector 选择器 , channel 通道 , buffer 缓冲区,模型如下:
Selector 多路复用器
选择器,也叫多路复用器,Java的NIO通过selector实现一个线程处理多个客户端链接,多个channel可以注册到同一个Selector,Selector能够监测到channel上是否有读/写事件发生,从而获取事件和对事件进行处理,所以Selector切到哪个channel是由事件决定的。当线程从某个客户端通道未读取到数据时,可以把空闲时间用来做其他任务,性能得到了提升。
Channel 通道
- 双向通信 : 传统的
InputStream
和OutputStream
是单向的,要么只能读取数据(InputStream
),要么只能写入数据(OutputStream
)。而Channel
可以同时进行读写操作。例如,FileChannel
可以从文件中读取数据,也可以将数据写入文件。 - 非阻塞IO :
Channel
结合Selector
可以实现非阻塞I/O操作,即在进行I/O操作时,线程不会被阻塞,可以继续执行其他任务。 - 与Buffer结合使用 :
Channel
的读写操作通常与Buffer
结合使用,数据在传输过程中都会经过Buffer
。Channel
从外部源(如文件、网络)读取数据到Buffer
,然后应用程序处理Buffer
中的数据;或者将应用程序的数据写入Buffer
,再通过Channel
将数据写到外部源。
常用的Channel类
FileChannel:主要用于文件的I/O操作,可以从文件中读取数据或将数据写入文件。
DatagramChannel:用于通过UDP协议进行数据的读写操作。UDP是一种无连接的、不可靠的传输协议,但它的传输效率较高。可以在非阻塞模式下发送和接收数据报文。不需要建立连接即可发送和接收数据。
SocketChannel:用于通过TCP协议进行客户端与服务器之间的网络通信。TCP是面向连接的、可靠的传输协议。可以在非阻塞模式下进行数据的读写。支持与服务器建立连接,并通过该连接进行数据传输。
Buffer 缓冲区
buffer主要是和channel通道做数据交互,Channel 提供从文件或网络读取数据的渠道,但是数据读取到一个它稍后处理的buffer中,实现了IO的非阻塞。 每个channel通道都会对应一个buffer,buffer是一个内存块,底层有一个数组,NIO的buffer可以写如数据,也可以从中读取数据。在Java中封装了很多基于buffer的类,如:
- ByteBuffer:存储字节数据到缓冲区
- ShortBuffer:存储字符串数据到缓冲区
- CharBuffer:存储字符数据到缓冲区
- IntBuffer:存储整数数据到缓冲区
- LongBuffer:存储长整型数据到缓冲区
- FloatBuffer:存储小数到缓冲区
- DoubleBuffer:存储小数到缓冲区
- MappedByteBuffer:基于内存操作文件
Buffer 的理解和使用
buffer : 缓冲区,buffer主要是和channel通道做数据交互,可以把数据写入Buffer以及从Buffer读取数据,java.nio.Buffer源码,以及常用子类如下:
Buffer可以看做是有一个数组来存储元素,Buffer类提供了四个很重要的属性:
-
capacity:Buffer所能够存放的最大容量,最多只能向 Buffer 写入 capacity 大小的字节
-
position:下一个被读或写的位置,随着不停的写入数据,position会向后移动,初始值是0,最大值是capacity - 1。当然在读数据的时候也需要知道读取的位置,当调用 flip 方法将 Buffer 从写模式转换为读模式时,position 被重新设置为 0 ,随着不停的读取,position会指向下个读取位置。
-
mark: 标记位置,用于记录某次读写的位置
-
limit: 对position的限制,在写模式下限制你能将多少数据写入Buffer中,limit等同于Buffer的容量(capacity)。当切换Buffer为读模式时,限制你最多能读取到多少数据。因此,当切换Buffer为读模式时,限制会被设置为写模式下的position值,即:你能读到之前写入的所有数据,限制被设置为已写的字节数,在写模式下就是position。
buffer使用数组存储元素,下面用 java.nio.ByteBuffer 来举例,源码如下:
java
public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>
{
// These fields are declared here rather than in Heap-X-Buffer in order to
// reduce the number of virtual method invocations needed to access these
// values, which is especially costly when coding small buffers.
//
//存储数据的byte数组
final byte[] hb; // Non-null only for heap buffers
final int offset;
boolean isReadOnly; // Valid only for heap buffers
这里看到了,ByteBuffer底层就是使用一个 final byte[] hb; 来存储元素,其他的Buffer是相同的道理。
创建容量为10字节的buffer: ByteBuffer byteBuffer = ByteBuffer.allocate(10);
写入三个字节的数据byteBuffer.put("aaa".getBytes());
调用读写转换方法 byteBuffer.flip();
读3个字节元素: byte[] bytes = new byte[4]; byteBuffer.get(bytes , 0 ,2);
Buffer API介绍
java
public abstract class Buffer {
//返回此缓冲区容量capacity
public final int capacity( )
//返回此缓冲区位置position
public final int position( )
//设置缓冲区的位置position
public final Buffer position (int newPositio)
//返回此缓冲区limit
public final int limit( )
//设置此缓冲区的限制limit
public final Buffer limit (int newLimit)
//在此缓冲区的位置设置标记
public final Buffer mark( )
//将此缓冲区的位置重置为以前标记的位置
//把position设置为mark
public final Buffer reset()
//清除此缓冲区, 即将各个标记恢复到初始状态
//把position设置为 0 ,limit = capacity;
public final Buffer clear( )
//读写反转此缓冲区
public final Buffer flip( )
//重置此缓冲区,position = 0; mark = -1;
public final Buffer rewind( )
//返回当前位置与限制之间的元素数
public final int remaining( )
//返回当前位置之后是否还有元素
public final boolean hasRemaining( )
//告知此缓冲区是否为只读缓冲区
public abstract boolean isReadOnly( );
//返回此缓冲区是否具有可访问的底层实现数组
public abstract boolean hasArray();
//返回此缓冲区的底层实现数组
public abstract Object array();
//返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量
public abstract int arrayOffset();
//告知此缓冲区是否为直接缓冲区
public abstract boolean isDirect();
}
public abstract class FileChannel
extends AbstractInterruptibleChannel
implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel
{
//从通道读取数据并放到缓冲区中
public int read(ByteBuffer dst) ;
//把缓冲区的数据写到通道中
public int write(ByteBuffer src) ;
//从目标通道中复制数据到当前通道
public long transferFrom(ReadableByteChannel src, long position, long count);
//把数据从当前通道复制给目标通道
public long transferTo(long position, long count, WritableByteChannel target);
}
Bytebuffer 简单使用
java
@Test
public void byteBufferTest(){
//创建一个容量 1024 的bytebuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//存储元素
byteBuffer.putChar('哈');
byteBuffer.putInt(123);
byteBuffer.putShort((short) 123);
//读写转换
byteBuffer.flip();
//获取元素
System.out.println(byteBuffer.getChar()); //哈
System.out.println(byteBuffer.getInt()); //123
System.out.println(byteBuffer.getLong()); //BufferUnderflowException 缓冲区溢出异常
}
使用buffer是需要注意,如果put的数据类型,和get是使用的类型不一致,可能会出现BufferUnderflowException
缓冲区溢出异常
写数据到文件
这个案例是通过Java把一段字符串写到磁盘的某个文件,它的大概流程示意图如下:
实现步骤如下:
- 把数据写入一个ByteBuffer缓冲区
- 创建一个FileOutputStream 输出流,目的是磁盘的一个文件
- 通过FileOutputStream得到FileChannel通道
- 调用channel.write,把ByteBuffer中的数据写入FileChannel,从而写到磁盘文件
实现代码如下:
java
//使用NIO向磁盘写一个文件
@Test
public void nioWriteTest() throws IOException {
//文件输出流
FileOutputStream fileOutputStream = new FileOutputStream("d:/1.txt");
//获取通道
FileChannel channel = fileOutputStream.getChannel();
//构建一个 容量1024字节长度的缓冲取
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
System.out.println(byteBuffer.getClass().getName());
//给buffer写入数据
byteBuffer.put("你好NIO".getBytes());
//转换
byteBuffer.flip();
//把buffer中的数据通过通道写入初盘文件
channel.write(byteBuffer);
//关闭通道
channel.close();
//关闭输出流
fileOutputStream.close();
}
从文件读数据
这个案例是通过Java把某个文件中数据读取到内存中,它的大概流程示意图如下:
实现步骤如下:
- 创建一个FileInputStream,目的是读取磁盘的某个文件
- 通过FileInputStream得到FileChannel
- 创建一个ByteBuffer用来接收数据
- 调用 channel.read 把数据写入bytebuffer
- 再从bytebuffer中得到真实的数据
实现代码如下:
java
@Test
public void nioReadTest() throws IOException {
//文件输入流
File file = new File("d:/1.txt");
FileInputStream fileInputStream = new FileInputStream(file);
//获取通道
FileChannel channel = fileInputStream.getChannel();
//创建一个buffer,容量为file的长度
ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length());
//从通道中读取数据到bytebuffer中
channel.read(byteBuffer);
//输出结果
System.out.println(new String(byteBuffer.array()));
//关闭资源
channel.close();
fileInputStream.close();
}
使用NIO完成文件拷贝
这个案例是通过NIO实现文件拷贝,它的大概流程示意图如下:
实现步骤如下:
- 创建一个FileInputStream,目的是读取磁盘的某个文件
- 通过FileInputStream得到FileChannel
- 创建一个ByteBuffer用来接收数据
- 调用 channel.read 把数据写入bytebuffer
- 创建FileOutputStream,目的是把数据写到另外一个文件
- 通过FileOutputStream得到FileChannel通道
- 调用channel.write,把ByteBuffer中的数据写入FileChannel,从而写到磁盘文件
实现代码如下:
java
//文件拷贝 1.txt 中的内容拷贝到2.txt
@Test
public void nioCopyTest() throws IOException {
//文件对象
File file = new File("d:/1.txt");
//文件输入流
FileInputStream fileInputStream = new FileInputStream(file);
//得到通道
FileChannel channel = fileInputStream.getChannel();
//文件输出流
FileOutputStream fileOutputStream = new FileOutputStream("d:/2.txt");
//获取通道
FileChannel outChannel = fileOutputStream.getChannel();
//缓冲区,容量为file的长度
ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length());
while(true){
//每次读取需要把缓冲区复位,否则当bytebuffer总的position等于limit的时候,
//read的返回值是 0 ,用于不会走-1,就会死循环
byteBuffer.clear();
//把数据读取到缓冲区
int readLenth = channel.read(byteBuffer);
System.out.println("redLength = "+readLenth);
//读取结果长度为-1说明读完了
if(readLenth == -1){
break;
}
//缓冲区读写交换
byteBuffer.flip();
outChannel.write(byteBuffer);
}
//关闭通道
channel.close();
//关闭流
fileInputStream.close();
//关闭通道
outChannel.close();
//关闭流
fileOutputStream.close();
}
使用transferFrom拷贝文件
FileChannel提供了 transferFrom方法可以实现通道和通道之间的数据拷贝,方法包括三个参数:
java
public abstract long transferFrom(ReadableByteChannel src,
long position, long count)throws IOException;
- src : 源通道,即从哪个通道拷贝数据
- position :拷贝的开始位置; 必须是非负数
- count : 要拷贝的最大字节数; 必须是非负数
实现代码如下:
java
//文件拷贝 1.txt 中的内容拷贝到2.txt
@Test
public void nioCopyTest2() throws IOException {
//读操作=================================================================================
//文件对象
File file = new File("d:/1.txt");
//文件输入流
FileInputStream fileInputStream = new FileInputStream(file);
//得到通道
FileChannel inputChannel = fileInputStream.getChannel();
//文件输出流
FileOutputStream fileOutputStream = new FileOutputStream("d:/2.txt");
//获取通道
FileChannel outChannel = fileOutputStream.getChannel();
//使用transferFrom拷贝数据,将inputChannel中数据拷贝到outChannel
outChannel.transferFrom(inputChannel, 0, inputChannel.size());
outChannel.close();
inputChannel.close();
fileInputStream.close();
fileOutputStream.close();
}
HeapByteBufferR只读buffer的使用
HeapByteBuffer,只读Buffer,只允许从中读数据,不允许写数据,否则抛出ReadOnlyBufferException异常,案例如下:
java
/**
* 只读buffer
*/
@Test
public void nioOnlyRead() throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
for (int i = 0 ; i < 10 ; i++){
byteBuffer.putInt(i); //0123456789
}
//读写转换
byteBuffer.flip();
//得到一个只读buffer ,使用的是java.nio.HeapByteBufferR
ByteBuffer readOnlyBuffer = byteBuffer.asReadOnlyBuffer();
//java.nio.HeapByteBufferR
System.out.println(readOnlyBuffer.getClass().getName());
while(readOnlyBuffer.hasRemaining()){
System.out.print(readOnlyBuffer.getInt()); //0123456789
}
readOnlyBuffer.putInt(10); //ReadOnlyBufferException ,不允许写
}
MappedByteBuffer 的使用
nio中引入了一种基于MappedByteBuffer操作大文件的方式,其读写性能极高,它可以基于内存实现文件的修改,这里的内存指的是"堆外内存"。
我们来做过案例,使用MappedByteBuffer来修改一个文本内容:"helloworld"把 h和w修改为大写。
java
@Test
public void mappedByteBuffer() throws IOException {
//随机访问文件,RW代表支持而读写,文件内容为 :helloworld
File file = new File("d:/3.txt");
RandomAccessFile randomAccessFile = new RandomAccessFile(file,"rw");
//通道
FileChannel channel = randomAccessFile.getChannel();
//得到MappedByteBuffer : mode:读写模式, position: 映射区域的起始位置 size: 映射区域大小
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, file.length());
//第1个字节修改为 大 H
mappedByteBuffer.put(0,(byte)'H');
//第6个字节修改为 大 W
mappedByteBuffer.put(5,(byte)'W');
randomAccessFile.close();
}
总结
本篇文件介绍了一下Java NIO 三大核心:selector , channel , buffer ,重点讲了Buffer的底层原理和几个小案例。
文章结束啦,如果对你有帮助的话,请一定给个好评哦~~~