文章目录
- 一、NIO介绍
- 二、Buffer(缓冲区)
- 1、常见Buffer子类
- 1.1、HeapByteBuffer
- 1.2、DirectByteBuffer
- 2、Buffer结构
- 3、常见方法
- 4、字符串与ByteBuffer互转
- 三、Channel(通道)
- 1、常见Channel实现类
- 2、FileChannel(文件通道)
- 2.1、常用方法
- 2.2、复制(transferTo/transferFrom)
- 3、ServerSocketChannel和SocketChannel(TCP网络通道)
- 3.1、阻塞模式
- 3.2、非阻塞模式
- 四、Selector(选择器)
- 1、Selector的应用
- 2、多路复用
- 五、零拷贝
- 1、传统IO
- 2、NIO优化
- 2.1、DirectByteBuffer
- 2.2、linux2.1提供的sendFile方法
- 2.3、linux 2.4
一、NIO介绍
NIO (New lO)也有人称之为java non-blocking lO是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java lO API。
NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区
的、基于通道的IO操作。NIO将以更加高效
的方式进行文件的读写操作
。
NIO可以理解为非阻塞IO
,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。
NIO相关类都被放在java.nio
包及子包下,并且对原java.io
包中的很多类进行改写。
NIO有三大核心部分: Buffer(缓冲区),Channel(通道),Selector(选择器)。
二、Buffer(缓冲区)
- 缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存
- 这块内存被包装成
NIO Buffer对象
,并提供了一组方法,用来方便的访问该块内存 - 缓冲区主要用于
与NIO通道进行交互
,数据是从通道读入缓冲区,从缓冲区写入通道中 - 所有缓冲区都是Buffer抽象类的子类.
1、常见Buffer子类
- ByteBuffer:用于存储字节数据(最常用)
- ShortBuffer:用于存储Short类型数据
- IntBuffer:用于存储Int类型数据
- LongBuffer:用于存储Long类型数据
- FloatBuffer:用于存储Float类型数据
- DoubleBuffer:用于存储Double类型数据
- CharBuffer:用于存储字符数据
ByteBuffer最常用,ByteBuffer三个子类的类图如下
1.1、HeapByteBuffer
- 存储内存是在
JVM堆
中分配 - 在堆中分配一个
数组
用来存放 Buffer 中的数据
public abstract class ByteBuffer
extends Buffer
implements Comparable<ByteBuffer>
{
//在堆中使用一个数组存放Buffer数据
final byte[] hb;
...
}
- 通过
allocate()
方法进行分配,在jvm堆上申请堆上内存 - 如果要做IO操作,会先从本进程的堆上内存复制到系统内存,再利用本地IO处理
- 读写效率较低,受到 GC 的影响
ByteBuffer heapByteBuffer = ByteBuffer.allocate(1024);
1.2、DirectByteBuffer
- DirectBuffer 背后的存储内存是在
堆外内存(操作系统内存)
中分配,jvm内存只保留堆外内存地址
public abstract class Buffer {
//堆外内存地址
long address;
...
}
- 通过
allocateDirect()
方法进行分配,直接从系统内存中申请 - 如果要作IO操作,直接从系统内存中利用本地IO处理
- 使用直接内存会具有更高的效率,但是它比申请普通的堆内存需要耗费更高的性能
- 读写效率高(少一次拷贝),不会受 GC 影响,分配的效率低
ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(1024);
2、Buffer结构
ByteBuffer 有以下重要属性
-
容量 (capacity) :作为一个内存块,Buffer具有一定的固定大小, 也称为"容量"
- 缓冲区容量不能为负,并且创建后
不能更改
- 缓冲区容量不能为负,并且创建后
-
限制 (limit):表示缓冲区中
可以操作数据的大小
(limit 后数据不能进行读写)- 缓冲区的限制不能为负,并且不能大于其容量
- 写入模式,限制等于 buffer的容量
- 读取模式下,limit等于写入的数据量
-
位置 (position):
下一个
要读取或写入的数据的索引- 缓冲区的位置不能为负,并且不能大于其限制
ByteBuffer写入和读取原理
@Test
public void simpleTest() {
// 1. 分配一个指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(10);
// 2. 利用put()存入数据到缓冲区中
buf.put("data".getBytes());
// 3. 切换读取数据模式
buf.flip();
// 判断缓冲区中是否还有元素
while (buf.hasRemaining()) {
// 4. 利用 get()读取单个字节
byte b = buf.get();
System.out.println("实际字节 " + (char) b);
}
// 清空缓冲区
buf.clear();
}
输出结果:
实际字节 d
实际字节 a
实际字节 t
实际字节 a
- 创建容量为10的ByteBuffer
- 写模式下,position 是写入位置,limit 等于容量,下图表示写入了 4 个字节后的状态
- flip 动作发生后,position 切换为读取位置,limit 切换为读取限制
- 读取 4 个字节后,状态如下
- clear 动作发生后,状态如下,然后切换至写模式
特别说明:compact方法,是把未读完的部分向前压缩,然后切换至写模式
3、常见方法
位置相关
int capacity()
:返回 Buffer 的 capacity 大小int limit()
:返回 Buffer 的界限(limit) 的位置int position()
:返回缓冲区的当前位置 position- int remaining() :返回 position 和 limit 之间的元素个数
@Test
public void test1() {
// 分配一个指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
System.out.println(buf.position());// 0: 表示当前的位置为0
System.out.println(buf.limit());// 1024: 表示界限为1024,前1024个位置是允许我们读写的
System.out.println(buf.capacity());// 1024:表示容量大小为1024
System.out.println(buf.remaining());// 1024:表示position和limit之间元素个数
}
读写相关
put(byte b)
:将给定单个字节写入缓冲区的当前位置put(byte[] src)
:将 src 中的字节写入缓冲区的当前位置- put(int index, byte b):将指定字节写入缓冲区的索引 位置(
不会移动 position
) - boolean hasRemaining(): 判断缓冲区中是否还有元素
get()
:读取单个字节get(byte[] dst)
:批量读取多个字节到 dst 中- get(int index):读取指定索引位置的字节(
不会移动 position
)
@Test
public void test2() {
ByteBuffer buf = ByteBuffer.allocate(10);
// 默认写模式,写入数据
buf.put("abcde".getBytes());
System.out.println(buf.position());// 5: 当前位置5,表示下一个写入的位置是5
System.out.println(buf.limit());// 10: 表示界限为10,前10个位置是允许写入的
// 切换为读模式
buf.flip();
System.out.println(buf.position());// 0: 从0位置开始读取数据
System.out.println(buf.limit());// 5: 表示界限为5,前5个位置是允许读取的
// 读取两个字节
byte[] dst = new byte[2];
buf.get(dst);
System.out.println(new String(dst, 0, 2)); // 输出:ab
System.out.println(buf.position());// 2: 从2位置开始读取数据,因为0,1已经读取
System.out.println(buf.limit());// 5: 表示界限为5,前5个位置是允许读取的
// 根据索引读取,position不会移动
byte b = buf.get(3);
System.out.println((char) b); // 输出:d
System.out.println(buf.position());// 2: 依然是2,没有移动
}
切换模式相关
Buffer flip()
:将缓冲区的界限设置为当前位置, 并将当前位置重置为0(切换为读模式)Buffer clear()
:清空缓冲区(切换为写模式)Buffer compact()
:向前压缩未读取部分(切换为写模式)
@Test
public void test3() {
ByteBuffer buf = ByteBuffer.allocate(10);
// 默认写模式,写入数据
buf.put("hello".getBytes());
// 切换为读模式
buf.flip();
// 读取两个字节
byte[] dst = new byte[2];
buf.get(dst);
System.out.println(buf.position());// 2: 当前位置2,前两个位置已经读取,读取下一个位置是2
System.out.println(buf.limit());// 5: 表示界限为5,前5个位置是允许读取的
// 向前压缩未读取,并切换为写模式
buf.compact();
System.out.println(buf.position());// 3: 当前位置3,因为之前有两个位置没有被读取,放到了最前面,写入的下一个位置是3
System.out.println(buf.limit());// 10: 表示界限为10,前10个位置是允许写入的
}
修改Buffer相关
- Buffer limit(int n):设置缓冲区界限为 n,并返回修改后的 Buffer 对象
- Buffer position(int n) :设置缓冲区的当前位置为 n, 并返回修改后的 Buffer 对象
标记相关
- Buffer mark(): 对缓冲区设置标记
- Buffer reset() :将位置 position 转到以前设置的mark 所在的位置
- Buffer rewind() :将位置设为为0, 取消设置的mark
@Test
public void test4() {
ByteBuffer buf = ByteBuffer.allocate(10);
// 默认写模式,写入数据
buf.put("hello".getBytes());
// 切换为读模式
buf.flip();
// 读取两个字节
System.out.println((char) buf.get());
buf.mark();
System.out.println((char) buf.get());
System.out.println((char) buf.get());
buf.reset();
System.out.println((char) buf.get());
System.out.println((char) buf.get());
System.out.println((char) buf.get());
System.out.println((char) buf.get());
// hello读完再读,抛异常java.nio.BufferUnderflowException
// System.out.println((char) buf.get());
}
输出:
h
e
l
e
l
l
o
总结Buffer读写数据四个步骤
- 写入数据到Buffer
- 调用flip()方法,转换为读取模式
- 从Buffer中读取数据
- 调用buffer.clear()方法或者buffer.compact()方法清除缓冲区并转换为写入模式
4、字符串与ByteBuffer互转
public class TestByteBufferString {
public static void main(String[] args) {
// 字符串转为ByteBuffer
// 方式一:put
ByteBuffer buffer1 = ByteBuffer.allocate(16);
buffer1.put("hello".getBytes());
// 方式二:Charset
ByteBuffer buffer2 = StandardCharsets.UTF_8.encode("hello");
// 方式三:wrap
ByteBuffer buffer3 = ByteBuffer.wrap("hello".getBytes());
// ByteBuffer转为字符串
// 方式一:Charset
String str1 = StandardCharsets.UTF_8.decode(buffer1).toString();
// 方式二:String
String str2 = new String(buffer2.array(), 0, buffer2.limit());
}
}
三、Channel(通道)
传统流是单向
的,只能读或者写,而NIO中的Channel(通道)是双向
的,可以读操作,也可以写操作。
1、常见Channel实现类
- FileChannel:用于读取、写入、映射和操作文件的通道
- DatagramChannel:通过UDP读写网络中的数据通道
-
ServerSocketChannel和SocketChannel:通过TCP读写网络中的数据的通道
- 类似于Socke和ServerSocket(阻塞IO),不同的是前者可以设置为非阻塞模式
2、FileChannel(文件通道)
- FileChannel只能工作在
阻塞
模式下
2.1、常用方法
获取FileChannel
不能直接打开 FileChannel,必须通过 FileInputStream、FileOutputStream 或者 RandomAccessFile 来获取 FileChannel,它们都有getChannel
方法。
- 通过
FileInputStream
获取的 channel 只能读 - 通过
FileOutputStream
获取的 channel 只能写 - 通过
RandomAccessFile
是否能读写根据构造RandomAccessFile时的读写模式决定
// 只能读
FileChannel channel1 = new FileInputStream("hello.txt").getChannel();
// 只能写
FileChannel channel2 = new FileOutputStream("hello.txt").getChannel();
// 以只读方式打开指定文件
FileChannel channel3 = new RandomAccessFile("hello.txt", "r").getChannel();
// 以读、写方式打开指定文件。如果该文件尚不存在,则尝试创建该文件
FileChannel channel4 = new RandomAccessFile("hello.txt", "rw").getChannel();
读取数据
int read(ByteBuffer dst)
:从Channel到中读取数据到ByteBuffer,返回值表示读到的字节数量
,-1
表示到达了文件的末尾
- long read(ByteBuffer[] dsts): 将Channel中的数据“分散”到ByteBuffer数组中
@Test
public void testRead() throws IOException {
// 获取只读文件通道
FileChannel channel = new RandomAccessFile("hello.txt", "r").getChannel();
// 创建字节缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
// 循环读取通道中的数据,并写入到 buf 中
while (channel.read(buf) != -1) {
// 缓存区切换到读模式
buf.flip();
// 读取 buf 中的数据
while (buf.position() < buf.limit()) {
// 将buf中的数据追加到文件中
System.out.println((char) buf.get());
}
// 清空已经读取完成的 buffer,以便后续使用
buf.clear();
}
// 关闭通道
channel.close();
}
写入数据
int write(ByteBuffer src)
:将ByteBuffer中的数据写入到Channel- long write(ByteBuffer[] srcs):将ByteBuffer数组中的数据“聚集”到 Channel
@Test
public void testRead() throws IOException {
// 获取写文件通道
FileChannel channel = new FileOutputStream("hello.txt").getChannel();
// 将ByteBuffer数据写到通道
channel.write(ByteBuffer.wrap("abc".getBytes()));
// 强制将数据刷出到物理磁盘
channel.force(false);
// 关闭通道
channel.close();
}
其他
long position()
:返回此通道的文件位置long size()
:返回此通道的文件的当前大小void force(boolean metaData)
:强制将所有对此通道的文件更新写入到存储设备中- FileChannel position(long p) :设置此通道的文件位置
- FileChannel truncate(long s) :将此通道的文件截取为给定大小
@Test
public void testOther() throws IOException {
// 获取写文件通道
FileChannel channel = new FileOutputStream("hello.txt").getChannel();
System.out.println(channel.position());// 0:当前位置为0,表示下次写入的位置为0
System.out.println(channel.size());// 0:文件大小为0
// 写入3个字符到 hello.txt 文件中
channel.write(ByteBuffer.wrap(("abc").getBytes()));
System.out.println(channel.position());// 3:当前位置为3,表示下次写入的位置为3
System.out.println(channel.size());// 3:文件大小为3,因为写入3个字符
channel.position(5);// 设置当前位置为5,表示下次写入的位置为5
// 再写入123,此时会跳过索引3和4,写入索引5
channel.write(ByteBuffer.wrap(("123").getBytes()));
// 将数据刷出到物理磁盘
channel.force(false);
// 关闭通道
channel.close();
}
输出结果:索引3和4的位置为空,这是应该特殊字符吧
2.2、复制(transferTo/transferFrom)
- 两个方式都能实现复制的功能
/**
* 方法一(目标文件调用者)
*/
@Test
public void transferFrom() throws Exception {
// 1、字节输入管道
FileInputStream is = new FileInputStream("hello.txt"); //