IO 处理流程图:
整个IO过程的流程如下:
1)程序员写代码创建一个缓冲区(这个缓冲区是用户缓冲区):然后在一个while循环里面调用read()方法读数据(触发"syscall read"系统调用)
byte[] b = new byte[4096];
while((read = inputStream.read(b))>=0) {
total = total + read;
// other code....
}
2)当执行到read()方法时,其实底层是发生了很多操作的:
①内核给磁盘控制器发命令说:我要读磁盘上的某某块磁盘块上的数据。--kernel issuing a command to the disk controller hardware to fetch the data from disk.
②在DMA的控制下,把磁盘上的数据读入到内核缓冲区。--The disk controller writes the data directly into a kernel memory buffer by DMA (Direct Memory Access,直接内存存取)
③内核把数据从内核缓冲区复制到用户缓冲区。--kernel copies the data from the temporary buffer in kernel space
这里的用户缓冲区应该就是我们写的代码中 new 的 byte[] 数组。
从上面的步骤中可以分析出什么?
ⓐ对于操作系统而言,JVM只是一个用户进程,处于用户态空间中。而处于用户态空间的进程是不能直接操作底层的硬件的。而IO操作就需要操作底层的硬件,比如磁盘。因此,IO操作必须得借助内核的帮助才能完成(中断,trap),即:用户态到内核态的切换。
ⓑ我们写代码 new byte[] 数组时,一般是都是“随意” 创建一个“任意大小”的数组。比如,new byte[128]、new byte[1024]、new byte[4096]....
但是,对于磁盘块的读取而言,每次访问磁盘读数据时,并不是读任意大小的数据的,而是:每次读一个磁盘块或者若干个磁盘块(这是因为访问磁盘操作代价是很大的,而且我们也相信局部性原理) 因此,就需要有一个“中间缓冲区”--即内核缓冲区。先把数据从磁盘读到内核缓冲区中,然后再把数据从内核缓冲区搬到用户缓冲区。
这也是为什么我们总感觉到第一次read操作很慢,而后续的read操作却很快的原因吧。因为,对于后续的read操作而言,它所需要读的数据很可能已经在内核缓冲区了,此时只需将内核缓冲区中的数据拷贝到用户缓冲区即可,并未涉及到底层的读取磁盘操作,当然就快了。
The kernel tries to cache and/or prefetch data, so the data being requested by the process may already be available in kernel space.
If so, the data requested by the process is copied out.
If the data isn't available, the process is suspended while the kernel goes about bringing the data into memory.
如果数据不可用,process将会被挂起,并需要等待内核从磁盘上把数据取到内核缓冲区中。
那我们可能会说:DMA为什么不直接将磁盘上的数据读入到用户缓冲区呢?一方面是 ⓑ中提到的内核缓冲区作为一个中间缓冲区。用来“适配”用户缓冲区的“任意大小”和每次读磁盘块的固定大小。另一方面则是,用户缓冲区位于用户态空间,而DMA读取数据这种操作涉及到底层的硬件,硬件一般是不能直接访问用户态空间的(OS的原因吧)
综上,由于DMA不能直接访问用户空间(用户缓冲区),普通IO操作需要将数据来回地在 用户缓冲区 和 内核缓冲区移动,这在一定程序上影响了IO的速度。那有没有相应的解决方案呢?
那就是直接内存映射IO,也即JAVA NIO中提到的内存映射文件,或者说 直接内存....总之,它们表达的意思都差不多。示例图如下:
从上图可以看出:内核空间的 buffer 与 用户空间的 buffer 都映射到同一块 物理内存区域。
它的主要特点如下:
①对文件的操作不需要再发read 或者 write 系统调用了---The user process sees the file data asmemory, so there is no need to issue read() or write() system calls.
②当用户进程访问“内存映射文件”地址时,自动产生缺页错误,然后由底层的OS负责将磁盘上的数据送到内存。关于页式存储管理,可参考:内存分配与内存管理的一些理解
As the user process touches the mapped memory space, page faults will be generated automatically to bring in the file data from disk.
If the user modifies the mapped memory space, the affected page is automatically marked as dirty and will be subsequently
flushed to disk to update the file.
这就是是JAVA NIO中提到的内存映射缓冲区(Memory-Mapped-Buffer)它类似于JAVA NIO中的直接缓冲区(Directed Buffer)。MemoryMappedBuffer可以通过java.nio.channels.FileChannel.java(通道)的 map方法创建。
使用内存映射缓冲区来操作文件,它比普通的IO操作读文件要快得多。甚至比使用文件通道(FileChannel)操作文件 还要快。因为,使用内存映射缓冲区操作文件时,没有显示的系统调用(read,write),而且OS还会自动缓存一些文件页(memory page)
Java NIO 与 IO 的主要区别:
Java NIO系统的核心在于:
通道(Channel)和缓冲区(Buffer)。
通道表示打开到 IO 设备(例如:文件、套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。
简而言之,Channel 负责传输, Buffer负责存储
缓冲区( (Buffer):
Buffer 就像一个数组,可以保存多个相同类型的数据。都是通过如下方法获取一个 Buffer
对象:
static XxxBuffer allocate(int capacity) : 创建一个容量为capacity 的 XxxBuffer 对象
根据数据类型不同(boolean 除外),提供了相应类型的缓冲区:
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
Buffer 中的重要概念:
容量 (capacity) :表示 Buffer 最大数据容量,缓冲区容量不能为负,并且创
建后不能更改。
限制 (limit) :第一个不应该读取或写入的数据的索引,即位于 limit 后的数据
不可读写。缓冲区的限制不能为负,并且不能大于其容量。
位置 (position):下一个要读取或写入的数据的索引。缓冲区的位置不能为
负,并且不能大于其限制
标记 (mark) 与重置 (reset) :标记是一个索引,通过 Buffer 中的 mark() 方法
指定 Buffer 中一个特定的 position,之后可以通过调用 reset() 方法恢复到这
个 position.
标记 、 位置 、 限制 、 容量遵守以下不变式: 0 <= mark <= position <= limit <= capacity
缓冲区的基本属性:
java 中直接缓冲区与非直接缓冲区:
字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在
机 此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),
虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法 来创建。此方法返回的 缓冲区进行分配和取消
分配所需成本通常高于 非直接缓冲区 。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对
应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
直接字节缓冲区还可以过 通过FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建 。该方法返回
MappedByteBuffer 。Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区
中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在
访问期间或稍后的某个时间导致抛出不确定的异常。
字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定。
提供此方法是为了能够在
性能关键型代码中执行显式缓冲区管理
区别:
非直接缓冲区:通过 allocate() 方法分配缓冲区,将缓冲区建立在 JVM 的内存中
直接缓冲区:通过 allocateDirect() 方法分配直接缓冲区,将缓冲区建立在物理内存中。可以提高效率
通道简述:
由 java.nio.channels 包定义的。Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过 Channel
本身不能直接访问数据,Channel 只能与Buffer 进行交互。
通道(Channel)
图一
图二
图三
在图一中,所有的IO操作都需要CPU深度参与,期间CPU无法参与其他工作。
在图二中,使用 (Direct Memory Access)DMA来完成IO操作,但还是需要CPU控制,在应用程序发出大量的IO请求时,可能会造成总线冲突的问题
在图三中,使用Channel,独立于CPU的处理器来完成IO操作
一、通道(Channel):用于源节点与目标节点的连接。在 Java NIO 中负责缓冲区中数据的传输。Channel 本身不存储数据,因此需要配合缓冲区进行传输。
Java 为 为 Channel 提供的主要实现类
• FileChannel:用于读取、写入、映射和操作文件的通道。
• DatagramChannel:通过 UDP 读写网络中的数据通道。
• SocketChannel:通过 TCP 读写网络中的数据。
• ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来
的连接都会创建一个 SocketChannel。
三、获取通道
获取通道的一种方式是对支持通道的对象调用
getChannel() 方法。支持通道的类如下:
FileInputStream
FileOutputStream
RandomAccessFile
DatagramSocket
Socket
ServerSocket
获取通道的其他方式是使用 Files 类的静态方法 newByteChannel() 获
取字节通道。或者通过通道的静态方法 open() 打开并返回指定通道。
2. 在 JDK 1.7 中的 NIO.2 针对各个通道提供了静态方法 open()
3. 在 JDK 1.7 中的 NIO.2 的 Files 工具类的 newByteChannel()
四、通道之间的数据传输
transferFrom()
transferTo()
JAVA NIO
阻塞非阻塞分析
BIO 中当Client 端的数据传输异常时, server端的处理数据的线程会处于阻塞状态,这样没有充分的利用CPU的资源。
java NIO 中增加Selector 来对Channel进行注册监控,只有当数据准备完成后,才会发送任务让Server端的线程执行,这样大大提高了CPU 的利用率。
java NIO 库中阻塞模型实现
这里的server端只有当client端把数据写入后才写入,在client写入之前一直处于阻塞状态
/*
* 一、使用 NIO 完成网络通信的三个核心:
*
* 1. 通道(Channel):负责连接
*
* java.nio.channels.Channel 接口:
* |--SelectableChannel
* |--SocketChannel
* |--ServerSocketChannel
* |--DatagramChannel
*
* |--Pipe.SinkChannel
* |--Pipe.SourceChannel
*
* 2. 缓冲区(Buffer):负责数据的存取
*
* 3. 选择器(Selector):是 SelectableChannel 的多路复用器。用于监控 SelectableChannel 的 IO 状况
*
*/
public class TestBlockingNIO {
/**
* 客户端
*/
@Test
public void client() throws IOException {
//1. 获取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
//2. 分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//3. 读取本地文件,并发送到服务端
while (inChannel.read(buf) != -1) {
buf.flip();
sChannel.write(buf);
buf.clear();
}
//4. 关闭通道
inChannel.close();
sChannel.close();
}
@Test
public void server() throws IOException {
//1.获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//2.绑定连接
serverSocketChannel.bind(new InetSocketAddress(9898));
//3. 获取客户端连接的通道
SocketChannel socketChannel = serverSocketChannel.accept();
//4. 分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//5. 接收客户端的数据,并保存到本地
while(socketChannel.read(buf) != -1) {
buf.flip();
outChannel.write(buf);
buf.clear();
}
//6. 关闭通道
socketChannel.close();
outChannel.close();
serverSocketChannel.close();
}
}
分散 (Scatter) 和聚集 (Gather)
分散读取(Scattering Reads)是指从 Channel 中读取的数据“分散”到多个 Buffer 中。
聚集写入(Gathering Writes)是指将多个 Buffer 中的数据“聚集”
到 Channel。
NIO 的非阻塞式网络通信
阻塞与非阻塞
传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write()
时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不
能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会
阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,
当服务器端需要处理大量客户端时,性能急剧下降。
Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数
据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时
间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入
和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同
时处理连接到服务器端的所有客户端。
选择器(Selector)
选择器(Selector) 是 SelectableChannle 对象的多路复用器,Selector 可
以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector
可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心。
SelectableChannle 的结构如下图:
选择 器(Selector )的应用
当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器
对通道的监听事件,需要通过第二个参数 ops 指定。
可以监听的事件类型(用 可使用 SelectionKey 的四个常量 表示):
读 : SelectionKey.OP_READ (1)
写 : SelectionKey.OP_WRITE (4)
连接 : SelectionKey.OP_CONNECT (8)
接收 : SelectionKey.OP_ACCEPT (16)
若注册时不止监听一个事件,则可以使用“位或”操作符连接。
例:
SelectionKey
当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器
对通道的监听事件,需要通过第二个参数 ops 指定。
可以监听的事件类型(用 可使用 SelectionKey 的四个常量 表示):
读 : SelectionKey.OP_READ (1)
写 : SelectionKey.OP_WRITE (4)
连接 : SelectionKey.OP_CONNECT (8)
接收 : SelectionKey.OP_ACCEPT (16)
若注册时不止监听一个事件,则可以使用“位或”操作符连接。
例:
SelectionKey
方 法 描 述
int interestOps() 获取感兴趣事件集合
int readyOps() 获取通道已经准备就绪的操作的集合
SelectableChannel channel() 获取注册通道
Selector selector() 返回选择器
boolean isReadable() 检测 Channal 中读事件是否就绪
boolean isWritable() 检测 Channal 中写事件是否就绪
boolean isConnectable() 检测 Channel 中连接是否就绪
boolean isAcceptable() 检测 Channel 中接收是否就绪
SelectionKey:表示 SelectableChannel 和 Selector 之间的注册关系。每次向
选择器注册通道时就会选择一个事件(选择键)。选择键包含两个表示为整
数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操
作。
Selector 的常用方法
java NIO中的SocketChannel是一个连接到TCP网
络套接字的通道。
Java NIO中的 ServerSocketChannel 是一个可以
监听新进来的TCP连接的通道,就像标准IO中
的ServerSocket一样。
实例:
public class TestNonBlockingNIO {
//客户端
public static void main(String[] args) throws IOException {
//1.获取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
//2.切换非阻塞模式
sChannel.configureBlocking(false);
//3.分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//4.发送数据给服务端
Scanner scan = new Scanner(System.in);
while (scan.hasNext()) {
String str = scan.next();
buf.put((new Date().toString() + "\n" + str).getBytes());
buf.flip();
sChannel.write(buf);
buf.clear();
}
//5.关闭通道
sChannel.close();
}
@Test
public void server() throws IOException {
//1. 获取通道
ServerSocketChannel ssChanel = ServerSocketChannel.open();
//2. 切换非阻塞模式
ssChanel.configureBlocking(false);
//3. 绑定连接
ssChanel.bind(new InetSocketAddress(9898));
//4. 获取选择器
Selector selector = Selector.open();
//5. 将通道注册到选择器上,并且指定“监听接收事件”
ssChanel.register(selector, SelectionKey.OP_ACCEPT);
//6. 轮询式的获取选择器上已经“准备就绪”的事件
while (selector.select() > 0) {
//7. 获取当前选择器中所有注册的"选择键(已就绪的监听事件"
Iterator it = selector.selectedKeys().iterator();
while (it.hasNext()) {
//8. 获取准备“就绪”的是事件
SelectionKey sk = it.next();
//9. 判断具体是什么事件准备就绪
if (sk.isAcceptable()) {
//10. 若"接收就绪",获取客户端连接
SocketChannel sChannel = ssChanel.accept();
//11. 切换非阻塞模式
sChannel.configureBlocking(false);
//12. 将该通道注册到选择器上
sChannel.register(selector, SelectionKey.OP_READ);
} else if (sk.isReadable()) {
//13. 获取当前选择器上"读就绪"状态的通道
SocketChannel sChannel = (SocketChannel) sk.channel();
//14. 读取数据
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = 0;
while ((len = sChannel.read(buf)) > 0){
buf.flip();
System.out.println(new String(buf.array(), 0 ,len));
buf.clear();
}
}
//15. 取消选择键 SelectionKey
it.remove();
}
}
}
}
DatagramChannel: Java NIO中的DatagramChannel是一个能收发
UDP包的通道。
实例:
/**
* 使用DatagramChannel 通道收发UDP 包
*/
public class TestNonBlockingNIO2 {
public static void main(String[] args) throws IOException {
//获取收发UDP 包的通道
DatagramChannel dc = DatagramChannel.open();
//设置为非阻塞
dc.configureBlocking(false);
//获取缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
Scanner scan = new Scanner(System.in);
while (scan.hasNext()) {
String str = scan.next();
//向缓冲区中写入数据
buffer.put((new Date().toString() + ":\n" + str).getBytes());
//切换到读模式
buffer.flip();
//发送消息
dc.send(buffer, new InetSocketAddress("127.0.0.1", 9898));
}
dc.close();
}
@Test
public void receive() throws IOException {
DatagramChannel dc = DatagramChannel.open();
dc.configureBlocking(false);
dc.bind(new InetSocketAddress(9898));
//获取选择器
Selector selector = Selector.open();
//将通道注册到选择器中,并配置选择器对通道的读事件进行监听
dc.register(selector, SelectionKey.OP_READ);
//轮询式的获取选择器上已经“准备就绪”的事件
while (selector.select() > 0) {
// 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
Iterator it = selector.selectedKeys().iterator();
//获取准备“就绪”的事件
while (it.hasNext()) {
SelectionKey sk = it.next();
//判断具体是什么事件准备就绪
if (sk.isReadable()) {
ByteBuffer buf = ByteBuffer.allocate(1024);
//接收数据
dc.receive(buf);
//切换为写数据模式
buf.flip();
System.out.println(new String(buf.array(), 0, buf.limit()));
buf.clear();
}
}
it.remove();
}
}
}
管道 (Pipe)
Java NIO 管道是2个线程之间的单向数据连接。Pipe有一个source
实例:
public class TestPipe {
/**
* java NIO 管道是两个线程之间的单向数据连接
* @throws IOException
*/
@Test
public void test1() throws IOException {
//1. 获取管道
Pipe pipe = Pipe.open();
//2. 将缓冲区中的数据写入管道
ByteBuffer buf = ByteBuffer.allocate(1024);
Pipe.SinkChannel sinkChannel = pipe.sink();
buf.put("通过单向管道发送数据".getBytes());
buf.flip();
sinkChannel.write(buf);
//3. 读取该缓冲区中的数据
Pipe.SourceChannel sourceChannel =pipe.source();
buf.flip();
int len = sourceChannel.read(buf);
System.out.println(new String(buf.array(), 0, len));
sourceChannel.close();
sinkChannel.close();
}
}