本篇主要介绍Java NIO的基本原理和主要组件
Netty是由JBOSS提供的Java开源网络应用程序框架,其底层是基于Java提供的NIO能力实现的。因此为了掌握Netty的底层原理,需要首先了解Java NIO的原理。
NIO简介
计算机主要由CPU、内存、外存、IO设备等硬件组成,计算机执行计算的过程就是CPU从内存中获取数据,进行计算,然后再将计算结果写入内存中。但由于内存非常昂贵且下电后数据会丢失,计算机需要使用外存来持久化存储大规模的数据,外存提供了大量的存储空间,代价是其存取速度远小于内存。除了读取外存数据,计算机还可以从网络设备获取网络中的数据,受网络传输速度的限制,计算机获取网络数据的速度也远小于其读取内存的速度。
对于存取这些低速IO设备,操作系统(如Linux)提供了5种不同的IO模型。对于Java来说,其最先提供的就是基于最简单的阻塞式IO模型实现的BIO(Blocking IO)库。当调用BIO库读取硬盘中的数据时,用户进程会一直被阻塞在读数据的接口上,直到数据被操作系统从硬盘中获取出来并返回给用户,这时用户进程才能继续向下执行。由于读取硬盘速度相比CPU计算速度慢很多,进程就会一直被卡在读取数据这里,用户体验就是进程没有响应。即使CPU处于空闲状态,也无法使用CPU进行其他工作。这就浪费了大量的资源,同时也给用户造成了不好的体验。
除了最传统的阻塞式IO,操作系统还提供了其他几种改进的IO模型,总体思想都是尽可能减少用户进程阻塞在IO上的时间,在进行慢速设备IO时,进程无需等待,可以继续处理其他指令,当数据获取完成时操作系统再通知用户进程可以进行后续的数据处理操作。因此Java在1.4版本后就推出了一套新的IO接口NIO(New IO),这套IO接口基于多路复用IO模型,提供了非阻塞的IO能力,因此NIO中的N也可以理解为Non-blocking。这套NIO接口实现了只用一个线程来轮询等待所有应用进程的IO就绪状态,当某个应用进程的IO状态就绪时,再通知对应进程进行数据读写的操作。这就避免了每个应用进程在IO时被阻塞,为开发高性能和高并发的应用提供了关键能力。
NIO的3个核心组件
- Channel
- Buffer
- Selector
Channel(通道)
Channel 是 NIO 的核心概念,它表示一个打开的连接,是数据读写的双向通道,这个连接可以连接到 I/O 设备(例如:磁盘文件,Socket)或者一个支持 I/O 访问的应用程序,Java NIO 使用缓冲区和通道来进行数据传输。
Channel的主要实现类有:
- FileChannel(读写文件)
- DatagramChannel(UDP编程)
- SocketChannel(TCP编程)
- ServerSocketChannel(TCP编程)
FileChannel
FileChannel只能工作在阻塞模式下,不能配合Selector
FileChannel不能直接打开,必须通过FileInputStream
,FileOutputStream
或RandomAccessFile
来获取,它们都有getChannel()
方法
- 通过
FileInputStream
获取的channel只能读 - 通过
FileOutputStream
获取的channel只能写 - 通过
RandomAccessFile
是否能读写根据构造时的读写模式决定
transferTo方法
效率相比使用流式方式拷贝数据高很多,底层使用了操作系统提供的零拷贝特性。
一次最多传输2g的数据
Buffer(缓冲区)
Buffer是NIO的另一个核心概念,NIO库操作数据都是通过缓冲区处理的,在数据读写的过程都要先经过缓冲区,然后再从缓冲区中按照块来处理数据。
从类图中可以看到,7 种数据类型对应着 7 种子类,这些名字是 Heap 开头子类,数据是存放在 JVM 堆中的。
MappedByteBuffer
MappedByteBuffer
存放在堆外直接内存中,可以与文件进行映射。
通过java.nio包和MappedByteBuffer允许Java程序直接从内存中读取文件内容,通过将整个或部分文件映射到内存,由操作系统来处理加载请求和写入文件,应用只需要和内存打交道,这使得IO操作非常快。
Mmap内存映射和普通标准IO操作的本质区别在于它并不需要将文件中的数据先拷贝至OS的内核IO缓冲区,而是可以直接将用户进程私有地址空间中的一块区域与文件对象建立映射关系,这样程序就好像可以直接从内存中完成对文件读/写操作一样。
ByteBuffer的正确使用方式
- 向buffer中写入数据,例如
channel.read(buf)
- 调用
flip()
切换成读模式 - 从buffer中读取数据,例如
buffer.get()
- 调用
clear()
或compact()
切换为写模式 - 重复1-4
点击查看代码
@Slf4j
public class TestByteBuffer {
public static void main(String[] args) {
try (FileInputStream fileInputStream = new FileInputStream("data.txt");
FileChannel channel = fileInputStream.getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate(10);
while (true) {
int len = channel.read(buffer);
if (len < 0) {
break;
}
log.info("读到的字符数:{}", len);
buffer.flip();
while (buffer.hasRemaining()) {
byte b = buffer.get();
log.info("读到的字符:{}", (char) b);
}
buffer.clear();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Java规定内码使用UTF-16编码,一个字符占用2个字节,也就是Java中的char类型在内存中是使用UTF-16的编码形式存储的。而我们代码中读取的文件data.txt使用的是UTF-8编码格式,因此对于其中的英文字符和数字,一个字符只占用一个字节。因此代码中我们可以每次读取一个字节,然后再把它转换成Java内部的char。
Buffer的结构
Buffer是由特定基本类型的元素组成的线性有限序列,除了Buffer里面的内容,其最重要的属性就是它的capacity
,limit
和position
。
capacity
:Buffer中可以储存的元素数量,Buffer的capacity
不能为负值也永远不会改变。limit
:Buffer中第一个不能被读取或写入的元素的位置,limit
不能为负值也不能大于capacity
。position
:Buffer中下一个将要被读取或写入的元素的位置,position
不能为负值也不能大于limit
。